From bc0e0f5ac1d7b044d403f859406b4b66d4b1ce9b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 7 Sep 2026 13:35:51 +0900 Subject: [PATCH 01/95] ksmbd: prevent stale data from being written by SMB2 WRITE requests Initialize the compatibility byte in request buffers without zeroing the entire allocation. Keep the one-byte compatibility exception for SMB2 messages so valid zero-length WRITE requests remain compatible, and validate the data range in the WRITE handlers against the current compound request before accessing it. This prevents stale request-buffer data from being written to files or sent to IPC pipes. Fixes: 443d61d1fa9f ("ksmbd: allocate one more byte for implied bcc[0]") Cc: stable@vger.kernel.org Reported-by: zdi-disclosures@trendmicro.com # ZDI-CAN-32436 Signed-off-by: Namjae Jeon --- fs/smb/server/compress.c | 1 + fs/smb/server/connection.c | 1 + fs/smb/server/smb2pdu.c | 27 ++++++++++++++++++--------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c index 5162fb84c755..3fcb3e44302e 100644 --- a/fs/smb/server/compress.c +++ b/fs/smb/server/compress.c @@ -65,6 +65,7 @@ static int __ksmbd_decompress_request(struct ksmbd_conn *conn, return -ENOMEM; *(__be32 *)out = cpu_to_be32(out_size); + out[out_size + 4] = 0; rc = smb_compression_decompress(conn->compress_algorithm, conn->compress_chained, conn->compress_pattern, diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index d211861ff86f..3f8cc4998da5 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -752,6 +752,7 @@ int ksmbd_conn_handler_loop(void *p) if (!conn->request_buf) break; + conn->request_buf[pdu_size + 4] = 0; memcpy(conn->request_buf, hdr_buf, sizeof(hdr_buf)); /* diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 18602a4759c0..c792ed5ebbd3 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9276,23 +9276,25 @@ static noinline int smb2_write_pipe(struct ksmbd_work *work) int err = 0, ret = 0; char *data_buf; size_t length; + unsigned int data_offset, req_len; WORK_BUFFERS(work, req, rsp); length = le32_to_cpu(req->Length); id = req->VolatileFileId; + data_offset = le16_to_cpu(req->DataOffset); + req_len = smb2_current_req_len(work, &req->hdr); - if ((u64)le16_to_cpu(req->DataOffset) + length > - get_rfc1002_len(work->request_buf)) { - pr_err("invalid write data offset %u, smb_len %u\n", - le16_to_cpu(req->DataOffset), - get_rfc1002_len(work->request_buf)); + if (data_offset < offsetof(struct smb2_write_req, Buffer) || + data_offset > req_len || length > req_len - data_offset) { + pr_err("invalid write data offset %u, length %zu, req_len %u\n", + data_offset, length, req_len); err = -EINVAL; goto out; } data_buf = (char *)(((char *)&req->hdr.ProtocolId) + - le16_to_cpu(req->DataOffset)); + data_offset); rpc_resp = ksmbd_rpc_write(work->sess, id, data_buf, length); if (rpc_resp) { @@ -9579,14 +9581,21 @@ int smb2_write(struct ksmbd_work *work) writethrough = true; if (is_rdma_channel == false) { - if (le16_to_cpu(req->DataOffset) < - offsetof(struct smb2_write_req, Buffer)) { + unsigned int data_offset = le16_to_cpu(req->DataOffset); + unsigned int req_len = smb2_current_req_len(work, &req->hdr); + + if (data_offset < offsetof(struct smb2_write_req, Buffer) || + data_offset > req_len || + length > req_len - data_offset) { + ksmbd_debug(SMB, + "invalid write data offset %u, length %zu, req_len %u\n", + data_offset, length, req_len); err = -EINVAL; goto out; } data_buf = (char *)(((char *)&req->hdr.ProtocolId) + - le16_to_cpu(req->DataOffset)); + data_offset); ksmbd_debug(SMB, "filename %pD, offset %lld, len %zu\n", fp->filp, offset, length); From d34d38ab0c62fe4eb9e746420c554e6d0e6f54db Mon Sep 17 00:00:00 2001 From: Henry Hu Date: Tue, 15 Sep 2026 22:08:34 +0800 Subject: [PATCH 02/95] ksmbd: fix iface_list use-after-free during server reset ksmbd_find_netdev_name_iface_list() walks iface_list for fsctl_query_iface_info_ioctl(), which holds RTNL. But during server reset, ksmbd_tcp_destroy() unlinks and frees the entries without RTNL, and queued SMB requests can still be running at that point. So an FSCTL_QUERY_NETWORK_INTERFACE_INFO request can therefore race with interface destruction and dereference a freed entry. With debug-only instrumentation to make the race deterministic, KASAN reports: BUG: KASAN: slab-use-after-free in ksmbd_find_netdev_name_iface_list ksmbd_find_netdev_name_iface_list fsctl_query_iface_info_ioctl smb2_ioctl handle_ksmbd_work Freed by: kfree ksmbd_tcp_destroy ksmbd_conn_transport_destroy kill_server_store The fix is to detach iface_list to a temporary list under RTNL, then drop RTNL before cleanup because the following cleanup may block and hold RTNL for a long time. Fixes: b2d99376c5d6 ("ksmbd: browse interfaces list on FSCTL_QUERY_INTERFACE_INFO IOCTL") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260911163516.6403-1-15815827059%40163.com Assisted-by: LLM Signed-off-by: Henry Hu Signed-off-by: Namjae Jeon --- fs/smb/server/transport_tcp.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 4968cfc1a572..e6f663acf5f8 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -656,10 +656,15 @@ static void ksmbd_tcp_stop_listener(struct interface *iface) void ksmbd_tcp_destroy(void) { struct interface *iface, *tmp; + LIST_HEAD(iface_list_to_free); unregister_netdevice_notifier(&ksmbd_netdev_notifier); - list_for_each_entry_safe(iface, tmp, &iface_list, entry) { + rtnl_lock(); + list_splice_init(&iface_list, &iface_list_to_free); + rtnl_unlock(); + + list_for_each_entry_safe(iface, tmp, &iface_list_to_free, entry) { ksmbd_tcp_stop_listener(iface); list_del(&iface->entry); kfree(iface->name); From 19d180c7baab7126af5848ed87d5f7b5add0cef2 Mon Sep 17 00:00:00 2001 From: Henry Hu Date: Wed, 9 Sep 2026 11:38:54 +0800 Subject: [PATCH 03/95] ksmbd: fix invalid pointer dereference when get_inode_acl() fails When get_inode_acl() fails, it returns an ERR_PTR(), but ksmbd_acls_fattr() stored that pointer in smb_fattr without checking for errors. Later callers then passed the error pointer to security descriptor helpers or posix_acl_release(), resulting in an invalid pointer dereference. This was reproduced on an XFS-backed share when xfs_get_acl() returned ERR_PTR(-ENOMEM). Querying the SMB ACL from a CIFS client triggered the crash: BUG: unable to handle page fault for address: fffffffffffffff8 RIP: smb_acl_sec_desc_scratch_len+0x58/0x90 [ksmbd] Call Trace: smb2_query_info+0x40d/0x17c0 [ksmbd] handle_ksmbd_work+0x1ca/0x770 [ksmbd] Fix this by checking the return values from get_inode_acl() in ksmbd_acls_fattr(). Treat -EOPNOTSUPP as indicating that no ACL is available, and propagate all other errors to the callers. Update smb2_open() and smb2_get_info_sec() to handle these errors. Cc: stable@vger.kernel.org Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Assisted-by: Codex:gpt-5.6-sol Reviewed-by: ChenXiaoSong Signed-off-by: Henry Hu Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 42 +++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index c792ed5ebbd3..05a3388a3453 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3647,10 +3647,11 @@ static int smb2_create_sd_buffer(struct ksmbd_work *work, le32_to_cpu(sd_buf->ccontext.DataLength), true, false); } -static void ksmbd_acls_fattr(struct smb_fattr *fattr, - const struct mnt_idmap *idmap, - struct inode *inode) +static int ksmbd_acls_fattr(struct smb_fattr *fattr, + const struct mnt_idmap *idmap, + struct inode *inode) { + struct posix_acl *acl; vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode); vfsgid_t vfsgid = i_gid_into_vfsgid(idmap, inode); @@ -3661,10 +3662,28 @@ static void ksmbd_acls_fattr(struct smb_fattr *fattr, fattr->cf_dacls = NULL; if (IS_ENABLED(CONFIG_FS_POSIX_ACL)) { - fattr->cf_acls = get_inode_acl(inode, ACL_TYPE_ACCESS); - if (S_ISDIR(inode->i_mode)) - fattr->cf_dacls = get_inode_acl(inode, ACL_TYPE_DEFAULT); + acl = get_inode_acl(inode, ACL_TYPE_ACCESS); + if (IS_ERR(acl)) { + if (acl != ERR_PTR(-EOPNOTSUPP)) + return PTR_ERR(acl); + acl = NULL; + } + fattr->cf_acls = acl; + + if (S_ISDIR(inode->i_mode)) { + acl = get_inode_acl(inode, ACL_TYPE_DEFAULT); + if (IS_ERR(acl)) { + if (acl != ERR_PTR(-EOPNOTSUPP)) { + posix_acl_release(fattr->cf_acls); + return PTR_ERR(acl); + } + acl = NULL; + } + fattr->cf_dacls = acl; + } } + + return 0; } enum { @@ -4791,7 +4810,10 @@ int smb2_open(struct ksmbd_work *work) int pntsd_size; size_t scratch_len; - ksmbd_acls_fattr(&fattr, idmap, inode); + rc = ksmbd_acls_fattr(&fattr, idmap, inode); + if (rc) + goto err_out; + scratch_len = smb_acl_sec_desc_scratch_len(&fattr, NULL, 0, OWNER_SECINFO | GROUP_SECINFO | @@ -7652,7 +7674,11 @@ static int smb2_get_info_sec(struct ksmbd_work *work, idmap = file_mnt_idmap(fp->filp); inode = file_inode(fp->filp); - ksmbd_acls_fattr(&fattr, idmap, inode); + rc = ksmbd_acls_fattr(&fattr, idmap, inode); + if (rc) { + ksmbd_fd_put(work, fp); + return rc; + } if (test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_ACL_XATTR)) From 2bc1d25fc8597fa6cbf6b1ff92653881939a6436 Mon Sep 17 00:00:00 2001 From: Henry Hu Date: Sat, 12 Sep 2026 00:35:13 +0800 Subject: [PATCH 04/95] ksmbd: fix link speed query in fsctl_query_iface_info_ioctl() fsctl_query_iface_info_ioctl() used to call netdev->ethtool_ops->get_link_ksettings() directly and never checked the return value. If the callback fails, cmd.base.speed may be left uninitialized. Also, SPEED_UNKNOWN (-1) is treated as a numeric link speed and would be converted into a bogus link speed. Use __ethtool_get_link_ksettings() to acquire the per-netdev ops lock when needed. Fall back to 1Gbps when the query fails, or when the reported speed is 0 or SPEED_UNKNOWN. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: Henry Hu Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 05a3388a3453..007a72005501 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10511,6 +10511,7 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, struct sockaddr_storage_rsp *sockaddr_storage; unsigned int flags; unsigned long long speed; + struct ethtool_link_ksettings cmd; rtnl_lock(); for_each_netdev(&init_net, netdev) { @@ -10545,10 +10546,8 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, nii_rsp->Next = cpu_to_le32(152); nii_rsp->Reserved = 0; - if (netdev->ethtool_ops->get_link_ksettings) { - struct ethtool_link_ksettings cmd; - - netdev->ethtool_ops->get_link_ksettings(netdev, &cmd); + if (!__ethtool_get_link_ksettings(netdev, &cmd) && + cmd.base.speed && cmd.base.speed != SPEED_UNKNOWN) { speed = cmd.base.speed; } else { ksmbd_debug(SMB, "%s %s\n", netdev->name, From a63bb4d91c784f63ad7c063717dff7920d8445be Mon Sep 17 00:00:00 2001 From: Henry Hu Date: Sat, 12 Sep 2026 00:35:14 +0800 Subject: [PATCH 05/95] ksmbd: fix unlocked IPv6 addr_list walk in fsctl_query_iface_info_ioctl() fsctl_query_iface_info_ioctl() walks idev6->addr_list with a plain list_for_each_entry() while holding only rtnl_lock(). rtnl does not serialize all writers of this list: the DAD-failure path (addrconf_dad_failure -> ipv6_add_addr) can insert addresses without holding RTNL. It updates idev->addr_list with list_add_tail_rcu() under write_lock_bh(&idev->lock), so it can race with the unlocked walk. addr_list is RCU managed, so walk it with list_for_each_entry_rcu() under rcu_read_lock(), matching the other lockless readers of the list. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260910090818.24479-1-15815827059%40163.com Co-developed-by: Xuanqiang Luo Signed-off-by: Xuanqiang Luo Signed-off-by: Henry Hu Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 007a72005501..8563b7aa4289 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10589,13 +10589,15 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, if (!idev6) continue; - list_for_each_entry(ifa, &idev6->addr_list, if_list) { + rcu_read_lock(); + list_for_each_entry_rcu(ifa, &idev6->addr_list, if_list) { if (ifa->flags & (IFA_F_TENTATIVE | IFA_F_DEPRECATED)) continue; memcpy(ipv6_addr, ifa->addr.s6_addr, 16); break; } + rcu_read_unlock(); sockaddr_storage->addr6.ScopeId = 0; nbytes += sizeof(struct network_interface_info_ioctl_rsp); } From 2c50a4a5e75db55880eca43c035649cb51fcb52a Mon Sep 17 00:00:00 2001 From: Henry Hu Date: Sat, 12 Sep 2026 00:35:15 +0800 Subject: [PATCH 06/95] ksmbd: fix invalid Next offset in interface info replies fsctl_query_iface_info_ioctl() assigns nii_rsp to the next output slot before checking whether the interface has an in_device or inet6_dev. If either lookup returns NULL, the loop continues without advancing nbytes, leaving nii_rsp pointing at an uncommitted slot. If no later entry is committed, clearing nii_rsp->Next updates that slot instead of the last returned entry, leaving the latter with a nonzero Next offset. Fix this by using nbytes to locate the last committed entry and clear its Next field, but only when the response contains at least one entry. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260910090818.24479-1-15815827059%40163.com Signed-off-by: Henry Hu 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 8563b7aa4289..d2e01bcbf222 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10604,9 +10604,15 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, } rtnl_unlock(); - /* zero if this is last one */ - if (nii_rsp) + /* + * nii_rsp may point to an entry not included in nbytes if the IPv4 + * or IPv6 device lookup failed. Clear Next in the last returned entry. + */ + if (nbytes > 0) { + nii_rsp = (struct network_interface_info_ioctl_rsp *) + &rsp->Buffer[nbytes - sizeof(*nii_rsp)]; nii_rsp->Next = 0; + } rsp->PersistentFileId = SMB2_NO_FID; rsp->VolatileFileId = SMB2_NO_FID; From bb0326a5806015fb7e5a6a721a50ff172c92c6e4 Mon Sep 17 00:00:00 2001 From: Henry Hu Date: Sat, 12 Sep 2026 00:35:16 +0800 Subject: [PATCH 07/95] ksmbd: report IPv4 and IPv6 addresses independently in iface query fsctl_query_iface_info_ioctl() appends an entry before checking that a usable address was found, so it advertises unspecified-address placeholders: an entry containing 0.0.0.0 when an in_device exists but has no primary IPv4 address, and an entry containing :: when an inet6_dev exists but its address list is empty or every address is tentative or deprecated. The loop also couples IPv6 reporting to IPv4 success: when __in_dev_get_rtnl() returns NULL it continues to the next interface, skipping the IPv6 pass, so an interface without an in_device is dropped even if it has a usable IPv6 address. Query IPv4 and IPv6 independently and append an entry only when an address is found. Factor out the common entry initialization and query the link speed once per interface instead of once per address family. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260910090818.24479-1-15815827059%40163.com Assisted-by: Codex:gpt-5.6-sol Signed-off-by: Henry Hu Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 131 ++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d2e01bcbf222..495c17dcf3f1 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10501,6 +10501,27 @@ static __be32 idev_ipv4_address(struct in_device *idev) return addr; } +static struct network_interface_info_ioctl_rsp * +ksmbd_iface_entry_init(struct smb2_ioctl_rsp *rsp, int nbytes, + struct net_device *netdev, unsigned long long speed) +{ + struct network_interface_info_ioctl_rsp *nii_rsp; + + nii_rsp = (struct network_interface_info_ioctl_rsp *)&rsp->Buffer[nbytes]; + nii_rsp->IfIndex = cpu_to_le32(netdev->ifindex); + nii_rsp->Capability = 0; + if (netdev->real_num_tx_queues > 1) + nii_rsp->Capability |= RSS_CAPABLE; + if (ksmbd_rdma_capable_netdev(netdev)) + nii_rsp->Capability |= RDMA_CAPABLE; + nii_rsp->Next = cpu_to_le32(152); + nii_rsp->Reserved = 0; + nii_rsp->LinkSpeed = cpu_to_le64(speed); + memset(nii_rsp->SockAddr_Storage, 0, 128); + + return nii_rsp; +} + static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, struct smb2_ioctl_rsp *rsp, unsigned int out_buf_len) @@ -10515,7 +10536,12 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, rtnl_lock(); for_each_netdev(&init_net, netdev) { - bool ipv4_set = false; + struct inet6_ifaddr *ifa; + struct inet6_dev *idev6; + struct in_device *idev; + struct in6_addr ip6 = { }; + bool have_ip6 = false; + __be32 ip4 = 0; if (netdev->type == ARPHRD_LOOPBACK) continue; @@ -10526,25 +10552,6 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, flags = netif_get_flags(netdev); if (!(flags & IFF_RUNNING)) continue; -ipv6_retry: - if (out_buf_len < - nbytes + sizeof(struct network_interface_info_ioctl_rsp)) { - rtnl_unlock(); - return -ENOSPC; - } - - nii_rsp = (struct network_interface_info_ioctl_rsp *) - &rsp->Buffer[nbytes]; - nii_rsp->IfIndex = cpu_to_le32(netdev->ifindex); - - nii_rsp->Capability = 0; - if (netdev->real_num_tx_queues > 1) - nii_rsp->Capability |= RSS_CAPABLE; - if (ksmbd_rdma_capable_netdev(netdev)) - nii_rsp->Capability |= RDMA_CAPABLE; - - nii_rsp->Next = cpu_to_le32(152); - nii_rsp->Reserved = 0; if (!__ethtool_get_link_ksettings(netdev, &cmd) && cmd.base.speed && cmd.base.speed != SPEED_UNKNOWN) { @@ -10554,60 +10561,66 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn, "speed is unknown, defaulting to 1Gb/sec"); speed = SPEED_1000; } - speed *= 1000000; - nii_rsp->LinkSpeed = cpu_to_le64(speed); - sockaddr_storage = (struct sockaddr_storage_rsp *) - nii_rsp->SockAddr_Storage; - memset(sockaddr_storage, 0, 128); - - if (!ipv4_set) { - struct in_device *idev; - - sockaddr_storage->Family = INTERNETWORK; - sockaddr_storage->addr4.Port = 0; - - idev = __in_dev_get_rtnl(netdev); - if (!idev) - continue; - sockaddr_storage->addr4.IPv4Address = - idev_ipv4_address(idev); - nbytes += sizeof(struct network_interface_info_ioctl_rsp); - ipv4_set = true; - goto ipv6_retry; - } else { - struct inet6_dev *idev6; - struct inet6_ifaddr *ifa; - __u8 *ipv6_addr = sockaddr_storage->addr6.IPv6Address; - - sockaddr_storage->Family = INTERNETWORKV6; - sockaddr_storage->addr6.Port = 0; - sockaddr_storage->addr6.FlowInfo = 0; - - idev6 = __in6_dev_get(netdev); - if (!idev6) - continue; + /* + * Query IPv4 and IPv6 independently; emit an entry only when a + * usable address exists, so an interface missing one family is + * still reported for the other and 0.0.0.0 / :: placeholders are + * never advertised. + */ + idev = __in_dev_get_rtnl(netdev); + if (idev) + ip4 = idev_ipv4_address(idev); + idev6 = __in6_dev_get(netdev); + if (idev6) { rcu_read_lock(); list_for_each_entry_rcu(ifa, &idev6->addr_list, if_list) { - if (ifa->flags & (IFA_F_TENTATIVE | - IFA_F_DEPRECATED)) + if (ifa->flags & (IFA_F_TENTATIVE | IFA_F_DEPRECATED)) continue; - memcpy(ipv6_addr, ifa->addr.s6_addr, 16); + memcpy(&ip6, ifa->addr.s6_addr, sizeof(ip6)); + have_ip6 = true; break; } rcu_read_unlock(); + } + + if (ip4) { + if (out_buf_len < + nbytes + sizeof(struct network_interface_info_ioctl_rsp)) { + rtnl_unlock(); + return -ENOSPC; + } + + nii_rsp = ksmbd_iface_entry_init(rsp, nbytes, netdev, speed); + sockaddr_storage = (struct sockaddr_storage_rsp *)nii_rsp->SockAddr_Storage; + sockaddr_storage->Family = INTERNETWORK; + sockaddr_storage->addr4.Port = 0; + sockaddr_storage->addr4.IPv4Address = ip4; + nbytes += sizeof(struct network_interface_info_ioctl_rsp); + } + + if (have_ip6) { + if (out_buf_len < + nbytes + sizeof(struct network_interface_info_ioctl_rsp)) { + rtnl_unlock(); + return -ENOSPC; + } + + nii_rsp = ksmbd_iface_entry_init(rsp, nbytes, netdev, speed); + sockaddr_storage = (struct sockaddr_storage_rsp *)nii_rsp->SockAddr_Storage; + sockaddr_storage->Family = INTERNETWORKV6; + sockaddr_storage->addr6.Port = 0; + sockaddr_storage->addr6.FlowInfo = 0; + memcpy(sockaddr_storage->addr6.IPv6Address, ip6.s6_addr, 16); sockaddr_storage->addr6.ScopeId = 0; nbytes += sizeof(struct network_interface_info_ioctl_rsp); } } rtnl_unlock(); - /* - * nii_rsp may point to an entry not included in nbytes if the IPv4 - * or IPv6 device lookup failed. Clear Next in the last returned entry. - */ + /* Clear Next of the last committed entry to terminate the list. */ if (nbytes > 0) { nii_rsp = (struct network_interface_info_ioctl_rsp *) &rsp->Buffer[nbytes - sizeof(*nii_rsp)]; From eb96e2a0e817807c48dc8387ca6a5249fdeae3a0 Mon Sep 17 00:00:00 2001 From: Giorgi Kobakhia Date: Fri, 11 Sep 2026 17:08:17 -0700 Subject: [PATCH 08/95] ksmbd: fix overflow in dacloffset bounds check in build_sec_desc() When the large dacl_offset is provided, dacl_offset + sizeof(struct smb_acl) overflows on 32-bit builds and the check is passed, Which makes le16_to_cpu(ppdacl_ptr->size) read out-of-bounds. This only happens for dacl_offset in range [0xFFFFFFF8, 0xFFFFFFFF]. So the oob read is max 8 bytes preceding the allocated ppntsd. unsigned int dacl_offset = le32_to_cpu(ppntsd->dacloffset); int ppdacl_size, ntacl_size = ppntsd_size - dacl_offset; if (!dacl_offset || (dacl_offset + sizeof(struct smb_acl) > ppntsd_size)) goto out; Commit beff0bc9d69b ("ksmbd: fix overflow in dacloffset bounds check") fixes similar checks in other functions. But leaves this one out, because it is only valid for 32-bit builds. Reproduced on 32-bit ARM with KASAN, by a client sending SMB2 SET_INFO(SEC) with dacloffset=0xfffffff8, then QUERY_INFO(SEC) on the same file: BUG: KASAN: slab-out-of-bounds in build_sec_desc (fs/smb/server/smbacl.c:1105) Read of size 2 at addr c2a273fa by task kworker/1:1/35 CPU: 1 UID: 0 PID: 35 Comm: kworker/1:1 Not tainted 7.3.0-rc2+ #5 Workqueue: ksmbd-io handle_ksmbd_work Call trace: kasan_check_range from build_sec_desc (fs/smb/server/smbacl.c:1105) build_sec_desc from smb2_query_info (fs/smb/server/smb2pdu.c:7676) smb2_query_info from handle_ksmbd_work (fs/smb/server/server.c:160) handle_ksmbd_work from process_one_work (kernel/workqueue.c:3401) process_one_work from worker_thread (kernel/workqueue.c:3473) worker_thread from kthread (kernel/kthread.c:436) Add overflow check. Fixes: 8f0541186e9a ("ksmbd: fix heap-based overflow in set_ntacl_dacl()") Cc: stable@vger.kernel.org Assisted-by: LLM claude Tested-by: Xiang Mei Signed-off-by: Giorgi Kobakhia Signed-off-by: Namjae Jeon --- fs/smb/server/smbacl.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index 05f93a4d9d82..cf8093c7e381 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -1096,9 +1096,12 @@ int build_sec_desc(const struct mnt_idmap *idmap, struct smb_acl *ppdacl_ptr; unsigned int dacl_offset = le32_to_cpu(ppntsd->dacloffset); int ppdacl_size, ntacl_size = ppntsd_size - dacl_offset; + size_t dacl_struct_end; if (!dacl_offset || - (dacl_offset + sizeof(struct smb_acl) > ppntsd_size)) + check_add_overflow(dacl_offset, sizeof(struct smb_acl), + &dacl_struct_end) || + dacl_struct_end > (size_t)ppntsd_size) goto out; ppdacl_ptr = (struct smb_acl *)((char *)ppntsd + dacl_offset); From 6a382d59995175136639bf502fd53050e45d5400 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Tue, 25 Aug 2026 13:51:06 +0000 Subject: [PATCH 09/95] smb/server: support compound fid in notify requests A Windows client can send a compound request containing: Create Request, File: ; Notify Request The Notify Request uses FFFF...FFFF as the compound FID. Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 495c17dcf3f1..e23ab02dabe2 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -11934,47 +11934,6 @@ int smb2_notify(struct ksmbd_work *work) return -EIO; } - /* - * 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); - 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; - } - /* * KSMBD does not implement a real change-notification backend. * Genuine SMB2 servers (and macOS smbfs) never complete a From fe16b8f4ce34bd741bca18897cadab0ef3117b19 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Sat, 29 Aug 2026 00:00:00 +0900 Subject: [PATCH 10/95] ksmbd: refactor smb2_notify() to a blocking wait The previous design registered a synthetic work struct (in_work) directly on conn->async_requests and deferred the response to a workqueue -- a bespoke async/cancel implementation duplicating what setup_async_work(), release_async_work(), and smb2_send_interim_resp() already provide for smb2_lock()'s pending byte-range lock. Replace it with that same pattern: setup_async_work() on the calling work itself, registered on fp->blocked_works, woken by cancel or by the handle closing via the existing set_close_state_blocked_works(). This removes the synthetic work struct, the notify_pendings list and its close-time drain, and the deferred workqueue send, leaving smb2_notify() sharing the same async/cancel machinery as smb2_lock() instead of its own separate copy. The worker now blocks on ksmbd_wq for as long as the watch stays open, instead of returning immediately. This also makes the skeleton ready for a future event-delivery implementation on the same blocking wait. Suggested-by: ChenXiaoSong 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/ksmbd_work.c | 3 - fs/smb/server/ksmbd_work.h | 5 +- fs/smb/server/smb2pdu.c | 343 +++++++++++-------------------------- fs/smb/server/vfs_cache.c | 48 ------ fs/smb/server/vfs_cache.h | 6 - 5 files changed, 99 insertions(+), 306 deletions(-) diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index d307aefe0aec..52ea1a674a67 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -57,7 +57,6 @@ 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; @@ -87,8 +86,6 @@ 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); 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 0844aa929f55..3a14e4d69aa1 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -91,8 +91,6 @@ 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; bool request_open_chseq_tracked:1; bool session_setup_reauth:1; @@ -115,9 +113,8 @@ struct ksmbd_work { struct list_head request_entry; /* List head at conn->async_requests */ struct list_head async_request_entry; + /* List head at ksmbd_file->blocked_works */ 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 e23ab02dabe2..0a069ac560e2 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -58,10 +58,6 @@ 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 \ @@ -1246,6 +1242,7 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) { struct smb2_hdr *rsp_hdr; struct ksmbd_work *in_work = ksmbd_alloc_work_struct(); + u16 command; if (!in_work) return; @@ -1269,6 +1266,23 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) smb2_set_err_rsp(in_work); rsp_hdr->Status = status; + /* + * Async interim responses are unsigned, but final responses must + * follow the normal signing rules. The synthetic work has no + * request buffer, so use the original work for request signing + * checks and the response header for SMB3 command selection. + */ + command = work->conn->ops->get_cmd_val(work); + if (status != STATUS_PENDING && !work->encrypted && work->sess && + work->conn->ops->set_sign_rsp && + (work->sess->sign || + (work->conn->ops->is_sign_req && + work->conn->ops->is_sign_req(work, command)))) { + in_work->sess = work->sess; + work->conn->ops->set_sign_rsp(in_work); + in_work->sess = NULL; + } + if (smb2_send_interim_work(in_work, work, true)) ksmbd_debug(SMB, "failed to send interim response\n"); ksmbd_free_work_struct(in_work); @@ -9743,7 +9757,6 @@ 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) @@ -9781,23 +9794,11 @@ int smb2_cancel(struct ksmbd_work *work) "smb2 with AsyncId %llu cancelled command = 0x%x\n", le64_to_cpu(hdr->Id.AsyncId), le16_to_cpu(chdr->Command)); - if (iter->cancel_fn == smb2_notify_cancel_fn) - cancelled_notify = - smb2_notify_cancel_claim(iter->cancel_argv); - else if (iter->cancel_fn) + 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; @@ -11774,137 +11775,26 @@ 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. 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; +struct ksmbd_notify_req { + wait_queue_head_t wait; }; -static void smb2_send_notify_cancelled(struct ksmbd_work *work) +/* + * Cancel handler for a pending CHANGE_NOTIFY. Called either by + * smb2_cancel() (conn->request_lock held, work->state already set to + * KSMBD_WORK_CANCELLED by the caller) or by + * set_close_state_blocked_works() (vfs_cache.c, fp->f_lock held, + * work->state already set to KSMBD_WORK_CLOSED by the caller) -- both + * callers hold a spinlock across this call, so it must not sleep. + * wake_up() only wakes the waiter in smb2_notify(); it does not touch + * fp->blocked_works itself, matching smb2_remove_blocked_lock()'s same + * non-mutating style for the equivalent byte-range-lock wait. + */ +static void smb2_notify_cancel(void **argv) { - struct smb2_hdr *hdr = smb_get_msg(work->response_buf); - struct ksmbd_conn *conn = work->conn; - struct ksmbd_session *sess; + struct ksmbd_notify_req *notify_req = argv[0]; - 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_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) -{ - struct ksmbd_work *in_work = (struct ksmbd_work *)argv[0]; - struct ksmbd_file *fp = (struct ksmbd_file *)argv[1]; - 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 NULL; - - /* 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; - 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_obj(*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; - } - 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); + wake_up(¬ify_req->wait); } /** @@ -11917,9 +11807,11 @@ 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; + struct ksmbd_notify_req notify_req; + struct ksmbd_file *fp = NULL; + void **argv = NULL; + bool async_work = false; + int err = 0; ksmbd_debug(SMB, "Received smb2 notify\n"); @@ -11930,123 +11822,83 @@ int smb2_notify(struct ksmbd_work *work) if (work->next_smb2_rcv_hdr_off && req->hdr.NextCommand) { rsp->hdr.Status = STATUS_INTERNAL_ERROR; - smb2_set_err_rsp(work); - return -EIO; + err = -EIO; + goto out; } - /* - * 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; + err = -ENOENT; + goto out; } - 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); + argv = kmalloc_obj(*argv, KSMBD_DEFAULT_GFP); + if (!argv) { rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; - smb2_set_err_rsp(work); - return 0; + err = -ENOMEM; + goto out; } + init_waitqueue_head(¬ify_req.wait); + argv[0] = ¬ify_req; + + err = setup_async_work(work, smb2_notify_cancel, argv); + if (err) { + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + goto out; + } + async_work = true; + /* - * 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. + * Handle close holds the file-table write lock while it marks the + * handle closed and walks blocked_works. Hold the matching read lock + * across the state check and registration so close cannot finish its + * walk between the lookup above and this list insertion. */ - 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; - } - 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); - ksmbd_fd_put(work, fp); - rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; - smb2_set_err_rsp(work); - return 0; + read_lock(&work->sess->file_table.lock); + if (fp->f_state != FP_INITED) { + read_unlock(&work->sess->file_table.lock); + rsp->hdr.Status = STATUS_NOTIFY_CLEANUP; + err = -ENOENT; + goto out; } + spin_lock(&fp->f_lock); + list_add_tail(&work->fp_entry, &fp->blocked_works); + spin_unlock(&fp->f_lock); + read_unlock(&work->sess->file_table.lock); smb2_send_interim_resp(work, STATUS_PENDING); - /* 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; - 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; - } - - 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); - 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; + err = wait_event_interruptible(notify_req.wait, + READ_ONCE(work->state) != KSMBD_WORK_ACTIVE); + if (err && READ_ONCE(work->state) == KSMBD_WORK_ACTIVE) { + /* + * Woken by a signal, not a real cancel/close. There is no + * notification backend yet to report anything else against, + * so treat this the same as a client-side cancel. + */ + WRITE_ONCE(work->state, KSMBD_WORK_CANCELLED); } spin_lock(&fp->f_lock); - list_add_tail(&in_work->notify_entry, &fp->notify_pendings); + list_del_init(&work->fp_entry); spin_unlock(&fp->f_lock); - ksmbd_fd_put(work, fp); + rsp->hdr.Status = work->state == KSMBD_WORK_CLOSED ? + STATUS_NOTIFY_CLEANUP : STATUS_CANCELLED; + smb2_send_interim_resp(work, rsp->hdr.Status); work->send_no_response = 1; - return 0; + +out: + if (rsp->hdr.Status != STATUS_SUCCESS && !work->send_no_response) + smb2_set_err_rsp(work); + if (async_work) + release_async_work(work); + else + kfree(argv); + if (fp) + ksmbd_fd_put(work, fp); + return err; } /** @@ -12241,11 +12093,12 @@ void smb3_set_sign_rsp(struct ksmbd_work *work) struct channel *chann; char signature[SMB2_CMACAES_SIZE]; struct kvec *iov; - u16 command = conn->ops->get_cmd_val(work); + u16 command; int n_vec; char *signing_key; hdr = ksmbd_resp_buf_curr(work); + command = le16_to_cpu(hdr->Command); if (command == SMB2_SESSION_SETUP_HE && (!conn->binding || hdr->Status != STATUS_SUCCESS)) { diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index fd2c595f0486..a96b764c4db5 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -617,7 +617,6 @@ 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); @@ -652,52 +651,6 @@ 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 @@ -1266,7 +1219,6 @@ 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 1884f6deb9d0..732ae26dd6a7 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -162,12 +162,6 @@ struct ksmbd_file { 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 - * STATUS_NOTIFY_CLEANUP when the handle is closed. - */ - struct list_head notify_pendings; }; static inline void set_ctx_actor(struct dir_context *ctx, From 6d24754c9154a6ad82996c4af23c04394d4a2bea Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 23 Aug 2026 18:11:36 +0900 Subject: [PATCH 11/95] ksmbd: doc: update SMB3 multichannel support status SMB3 request replay support is now available. Update the ksmbd documentation to reflect that SMB3 multichannel is supported. Signed-off-by: Namjae Jeon --- Documentation/filesystems/smb/ksmbd.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Documentation/filesystems/smb/ksmbd.rst b/Documentation/filesystems/smb/ksmbd.rst index 672c5d3892ff..050af25d5339 100644 --- a/Documentation/filesystems/smb/ksmbd.rst +++ b/Documentation/filesystems/smb/ksmbd.rst @@ -82,8 +82,7 @@ Signing Update Supported. Pre-authentication integrity Supported. SMB3 encryption(CCM, GCM) Supported. (CCM/GCM128 and CCM/GCM256 supported) SMB direct(RDMA) Supported. -SMB3 Multi-channel Partially Supported. Planned to implement - replay/retry mechanisms for future. +SMB3 Multi-channel Supported. Receive Side Scaling mode Supported. SMB3.1.1 POSIX extension Supported. ACLs Partially Supported. only DACLs available, SACLs From 9d390dca544f0d40066b7dd8f859198ea3cd1299 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 23 Aug 2026 18:11:37 +0900 Subject: [PATCH 12/95] ksmbd: doc: update RDMA feature support status ksmbd supports SMB3 encryption over RDMA, while signing over RDMA is still under development. Update the feature status table accordingly. Signed-off-by: Namjae Jeon --- Documentation/filesystems/smb/ksmbd.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/filesystems/smb/ksmbd.rst b/Documentation/filesystems/smb/ksmbd.rst index 050af25d5339..2425f321f5b7 100644 --- a/Documentation/filesystems/smb/ksmbd.rst +++ b/Documentation/filesystems/smb/ksmbd.rst @@ -112,7 +112,8 @@ ksmbd/nfsd interoperability Planned for future. The features that ksmbd support are Leases, Notify, ACLs and Share modes. SMB3.1.1 Compression Supported. SMB3.1.1 over QUIC Planned for future. -Signing/Encryption over RDMA Planned for future. +Signing over RDMA Under development. +Encryption over RDMA Supported. SMB3.1.1 GMAC signing support Planned for future. ============================== ================================================= From 1946f99c218b1eeef1086a3d1623f3af01065c9b Mon Sep 17 00:00:00 2001 From: Hang Nan Date: Wed, 19 Aug 2026 11:30:11 +0800 Subject: [PATCH 13/95] ksmbd: add KUnit test for the DACL walk boundary smb_check_perm_dacl() must stop walking ACEs at the DACL declared size instead of using the enclosing security descriptor length. Add the ksmbd KUnit test configuration and a semantic harness that verifies a crafted access-granting ACE beyond the declared DACL size is ignored. Suggested-by: ChenXiaoSong Suggested-by: Namjae Jeon Signed-off-by: Hang Nan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/Kconfig | 2 + fs/smb/server/Makefile | 1 + fs/smb/server/tests/Kconfig | 15 +++ fs/smb/server/tests/Makefile | 4 + fs/smb/server/tests/smbacl_kunit.c | 170 +++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 fs/smb/server/tests/Kconfig create mode 100644 fs/smb/server/tests/Makefile create mode 100644 fs/smb/server/tests/smbacl_kunit.c diff --git a/fs/smb/server/Kconfig b/fs/smb/server/Kconfig index 221ec9717a83..b7665e0e4942 100644 --- a/fs/smb/server/Kconfig +++ b/fs/smb/server/Kconfig @@ -71,3 +71,5 @@ config SMB_SERVER_KERBEROS5 bool "Support for Kerberos 5" depends on SMB_SERVER default y + +source "fs/smb/server/tests/Kconfig" diff --git a/fs/smb/server/Makefile b/fs/smb/server/Makefile index a3e9306055e8..9bc87695a53c 100644 --- a/fs/smb/server/Makefile +++ b/fs/smb/server/Makefile @@ -19,3 +19,4 @@ $(obj)/ksmbd_spnego_negtokentarg.asn1.o: $(obj)/ksmbd_spnego_negtokentarg.asn1.c ksmbd-$(CONFIG_SMB_SERVER_SMBDIRECT) += transport_rdma.o ksmbd-$(CONFIG_PROC_FS) += proc.o +obj-$(CONFIG_SMB_SERVER_KUNIT_TESTS) += tests/ diff --git a/fs/smb/server/tests/Kconfig b/fs/smb/server/tests/Kconfig new file mode 100644 index 000000000000..ad7a4e94ceaa --- /dev/null +++ b/fs/smb/server/tests/Kconfig @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Hang Nan + +config SMB_SERVER_KUNIT_TESTS + tristate "KUnit tests for SMB3 server helpers" if !KUNIT_ALL_TESTS + depends on SMB_SERVER && SMB_KUNIT_TESTS && TMPFS_XATTR + default SMB_KUNIT_TESTS + help + This builds the KUnit tests for ksmbd server helpers. The tests + exercise internal server functionality and help detect regressions + in server-side behavior. They are intended for kernel developers + and are not suitable for production systems. + + For more information on KUnit and unit tests in the kernel, + please read Documentation/dev-tools/kunit/index.rst. diff --git a/fs/smb/server/tests/Makefile b/fs/smb/server/tests/Makefile new file mode 100644 index 000000000000..8738ab0b0667 --- /dev/null +++ b/fs/smb/server/tests/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Hang Nan + +obj-$(CONFIG_SMB_SERVER_KUNIT_TESTS) += smbacl_kunit.o diff --git a/fs/smb/server/tests/smbacl_kunit.c b/fs/smb/server/tests/smbacl_kunit.c new file mode 100644 index 000000000000..733c2fa92030 --- /dev/null +++ b/fs/smb/server/tests/smbacl_kunit.c @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * KUnit tests for ksmbd security descriptor (DACL) handling. + * + * Copyright (C) 2026 Hang Nan + * + * The tests pin the DACL declared-size boundary in smb_check_perm_dacl(): + * + * - ksmbd_dacl_walk_must_stop_at_declared_size: a pure semantic harness + * that models the ACE walk. Walking to the end of the enclosing + * security descriptor (the pre-fix behaviour) selects an ACE that + * sits beyond struct smb_acl::size; stopping at the declared DACL + * size (the fixed behaviour) rejects it. + */ + +#include +#include + +#include "../smbacl.h" +#include "../smb_common.h" + +struct ksmbd_acl_walk_result { + bool found; + bool allowed; + const struct smb_ace *selected; +}; + +static const struct smb_sid test_nonmatching_sid = { + 1, 5, {0, 0, 0, 0, 0, 5}, + { cpu_to_le32(21), cpu_to_le32(1), cpu_to_le32(2), + cpu_to_le32(3), cpu_to_le32(9999) } +}; + +/* + * S-1-22-1-0: the SID id_to_sid(0, SIDUNIX_USER) resolves to, i.e. what + * smb_check_perm_dacl() looks for when called with uid == 0. + */ +static const struct smb_sid test_owner_sid = { + 1, 2, {0, 0, 0, 0, 0, 22}, + { cpu_to_le32(1), cpu_to_le32(0) } +}; + +static int test_compare_sids(const struct smb_sid *a, const struct smb_sid *b) +{ + int i; + + if (a->revision != b->revision || a->num_subauth != b->num_subauth) + return 1; + for (i = 0; i < NUM_AUTHS; i++) { + if (a->authority[i] != b->authority[i]) + return 1; + } + for (i = 0; i < a->num_subauth; i++) { + if (a->sub_auth[i] != b->sub_auth[i]) + return 1; + } + return 0; +} + +static u16 test_ace_size(const struct smb_sid *sid) +{ + return offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE + + sid->num_subauth * sizeof(__le32); +} + +static u16 fill_test_ace(struct smb_ace *ace, const struct smb_sid *sid, + u32 access_req) +{ + u16 size = test_ace_size(sid); + + ace->type = ACCESS_ALLOWED_ACE_TYPE; + ace->flags = 0; + ace->size = cpu_to_le16(size); + ace->access_req = cpu_to_le32(access_req); + memcpy(&ace->sid, sid, size - offsetof(struct smb_ace, sid)); + return size; +} + +static struct ksmbd_acl_walk_result test_walk_dacl(struct smb_acl *pdacl, + int walk_boundary, + const struct smb_sid *target, + u32 requested) +{ + struct ksmbd_acl_walk_result result = {}; + struct smb_ace *ace; + int aces_size; + int i; + + ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); + aces_size = walk_boundary - sizeof(struct smb_acl); + for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { + u16 ace_size; + + if (aces_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE) + break; + ace_size = le16_to_cpu(ace->size); + if (ace_size > aces_size || + ace_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE) + break; + aces_size -= ace_size; + + 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 (!test_compare_sids(target, &ace->sid)) { + result.found = true; + result.selected = ace; + result.allowed = !(requested & ~le32_to_cpu(ace->access_req)); + return result; + } + + ace = (struct smb_ace *)((char *)ace + ace_size); + } + + return result; +} + +static void ksmbd_dacl_walk_must_stop_at_declared_size(struct kunit *test) +{ + struct ksmbd_acl_walk_result declared, enclosing; + struct smb_acl *acl; + struct smb_ace *ace1, *fake; + u16 ace1_size, fake_size; + u16 pdacl_size; + u16 acl_size; + + acl = kunit_kzalloc(test, 128, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, acl); + + acl->revision = cpu_to_le16(2); + acl->num_aces = cpu_to_le16(2); + + ace1 = (struct smb_ace *)((char *)acl + sizeof(*acl)); + ace1_size = fill_test_ace(ace1, &test_nonmatching_sid, 0); + fake = (struct smb_ace *)((char *)ace1 + ace1_size); + fake_size = fill_test_ace(fake, &test_owner_sid, FILE_READ_DATA); + + pdacl_size = sizeof(*acl) + ace1_size; + acl_size = pdacl_size + fake_size; + acl->size = cpu_to_le16(pdacl_size); + + declared = test_walk_dacl(acl, pdacl_size, &test_owner_sid, + FILE_READ_DATA); + enclosing = test_walk_dacl(acl, acl_size, &test_owner_sid, + FILE_READ_DATA); + + KUNIT_EXPECT_FALSE(test, declared.found); + KUNIT_EXPECT_FALSE(test, declared.allowed); + + /* Demonstrates that the buggy acl_size boundary selects fake ACE #2. */ + KUNIT_EXPECT_TRUE(test, enclosing.found); + KUNIT_EXPECT_TRUE(test, enclosing.allowed); +} + +static struct kunit_case ksmbd_smbacl_test_cases[] = { + KUNIT_CASE(ksmbd_dacl_walk_must_stop_at_declared_size), + {} +}; + +static struct kunit_suite ksmbd_smbacl_test_suite = { + .name = "ksmbd-smbacl", + .test_cases = ksmbd_smbacl_test_cases, +}; + +kunit_test_suite(ksmbd_smbacl_test_suite); + +MODULE_DESCRIPTION("KUnit tests for ksmbd smbacl helpers"); +MODULE_LICENSE("GPL"); From e72d86089f8eff59abee1ad53af4b35841f46dd0 Mon Sep 17 00:00:00 2001 From: Hang Nan Date: Mon, 24 Aug 2026 12:29:32 +0900 Subject: [PATCH 14/95] ksmbd: test smb_check_perm_dacl() DACL walk boundary Drive smb_check_perm_dacl() through ksmbd's NTACL xattr path with a crafted descriptor whose second ACE is beyond the declared DACL size. Verify that the out-of-boundary ACE is not selected and access remains denied. Suggested-by: ChenXiaoSong Signed-off-by: Hang Nan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smbacl.c | 2 + fs/smb/server/tests/smbacl_kunit.c | 90 ++++++++++++++++++++++++++++++ fs/smb/server/vfs.c | 2 + 3 files changed, 94 insertions(+) diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index cf8093c7e381..96428df33b43 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -1668,6 +1669,7 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, kfree(pntsd); return rc; } +EXPORT_SYMBOL_IF_KUNIT(smb_check_perm_dacl); int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon, const struct path *path, struct smb_ntsd *pntsd, int ntsd_len, diff --git a/fs/smb/server/tests/smbacl_kunit.c b/fs/smb/server/tests/smbacl_kunit.c index 733c2fa92030..391b1f5d181c 100644 --- a/fs/smb/server/tests/smbacl_kunit.c +++ b/fs/smb/server/tests/smbacl_kunit.c @@ -11,13 +11,22 @@ * security descriptor (the pre-fix behaviour) selects an ACE that * sits beyond struct smb_acl::size; stopping at the declared DACL * size (the fixed behaviour) rejects it. + * + * - ksmbd_smb_check_perm_dacl_boundary: drives the real + * smb_check_perm_dacl() with a descriptor stored through ksmbd's own + * NTACL xattr path on a tmpfs file, and asserts that a post-boundary + * ACE is not selected for a regular access check. */ #include +#include +#include +#include #include #include "../smbacl.h" #include "../smb_common.h" +#include "../vfs.h" struct ksmbd_acl_walk_result { bool found; @@ -154,8 +163,88 @@ static void ksmbd_dacl_walk_must_stop_at_declared_size(struct kunit *test) KUNIT_EXPECT_TRUE(test, enclosing.allowed); } +/* + * Build an NTSD whose DACL declares one ACE (pdacl->size) but actually + * contains two: the second ACE sits beyond the declared DACL boundary + * yet inside the enclosing security descriptor. The trailing ACE applies + * to S-1-22-1-0, which smb_check_perm_dacl() looks for when uid is zero. + */ +static struct smb_ntsd *build_boundary_ntsd(struct kunit *test, + const struct smb_sid *first_sid, + u32 first_access, + u32 trailing_access, + int *ntsd_size) +{ + struct smb_ntsd *pntsd; + struct smb_acl *pdacl; + struct smb_ace *ace; + u16 first_size = test_ace_size(first_sid); + u16 trailing_size = test_ace_size(&test_owner_sid); + + *ntsd_size = sizeof(struct smb_ntsd) + sizeof(struct smb_acl) + + first_size + trailing_size; + pntsd = kunit_kzalloc(test, *ntsd_size, GFP_KERNEL); + if (!pntsd) + return NULL; + + pntsd->revision = cpu_to_le16(SD_REVISION); + pntsd->type = cpu_to_le16(DACL_PRESENT); + pntsd->dacloffset = cpu_to_le32(sizeof(struct smb_ntsd)); + + pdacl = (struct smb_acl *)((char *)pntsd + sizeof(struct smb_ntsd)); + pdacl->revision = cpu_to_le16(2); + pdacl->num_aces = cpu_to_le16(2); + pdacl->size = cpu_to_le16(sizeof(struct smb_acl) + first_size); + + ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); + fill_test_ace(ace, first_sid, first_access); + + ace = (struct smb_ace *)((char *)ace + first_size); + fill_test_ace(ace, &test_owner_sid, trailing_access); + + return pntsd; +} + +static void ksmbd_smb_check_perm_dacl_boundary_test(struct kunit *test) +{ + struct file *file; + struct smb_ntsd *pntsd; + __le32 daccess = cpu_to_le32(FILE_READ_DATA); + int ntsd_size, rc; + + pntsd = build_boundary_ntsd(test, &test_nonmatching_sid, 0, + FILE_READ_DATA, &ntsd_size); + KUNIT_ASSERT_NOT_NULL(test, pntsd); + + file = shmem_file_setup("ksmbd-kunit-dacl", 0, + mk_vma_flags(VMA_NORESERVE_BIT)); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, file); + + rc = ksmbd_vfs_set_sd_xattr(NULL, mnt_idmap(file->f_path.mnt), + &file->f_path, pntsd, ntsd_size, + false); + KUNIT_EXPECT_EQ(test, 0, rc); + if (rc) + goto out; + + rc = smb_check_perm_dacl(NULL, &file->f_path, &daccess, + cpu_to_le32(FILE_READ_DATA), 0, false); + + /* + * The post-boundary ACE (ACE #2, beyond pdacl->size) grants + * FILE_READ_DATA to the caller's SID, but it must not be + * selected: the walk stops at the declared DACL size and access + * is denied. Before the fix the walk used the enclosing + * descriptor length, selected ACE #2 and returned 0. + */ + KUNIT_EXPECT_EQ(test, -EACCES, rc); +out: + fput(file); +} + static struct kunit_case ksmbd_smbacl_test_cases[] = { KUNIT_CASE(ksmbd_dacl_walk_must_stop_at_declared_size), + KUNIT_CASE(ksmbd_smb_check_perm_dacl_boundary_test), {} }; @@ -168,3 +257,4 @@ kunit_test_suite(ksmbd_smbacl_test_suite); MODULE_DESCRIPTION("KUnit tests for ksmbd smbacl helpers"); MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING"); diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 5cb161994e70..3bc9ef455faf 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -1671,6 +1672,7 @@ int ksmbd_vfs_set_sd_xattr(struct ksmbd_conn *conn, kfree(def_smb_acl); return rc; } +EXPORT_SYMBOL_IF_KUNIT(ksmbd_vfs_set_sd_xattr); int ksmbd_vfs_get_sd_xattr(struct ksmbd_conn *conn, const struct mnt_idmap *idmap, From c281289daef305a0fb16992fe7f69c3092ce7acf Mon Sep 17 00:00:00 2001 From: Hang Nan Date: Wed, 19 Aug 2026 11:30:13 +0800 Subject: [PATCH 15/95] ksmbd: test maximal-access DACL walk boundary Add a maximal-access variant of the smb_check_perm_dacl() boundary test. The in-boundary ACE grants read access, while a trailing ACE beyond the declared DACL size grants write access. Verify that maximal-access calculation includes the in-boundary permission and ignores the trailing permission. Suggested-by: ChenXiaoSong Signed-off-by: Hang Nan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/tests/smbacl_kunit.c | 47 ++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/tests/smbacl_kunit.c b/fs/smb/server/tests/smbacl_kunit.c index 391b1f5d181c..33496b4d31a3 100644 --- a/fs/smb/server/tests/smbacl_kunit.c +++ b/fs/smb/server/tests/smbacl_kunit.c @@ -12,10 +12,11 @@ * sits beyond struct smb_acl::size; stopping at the declared DACL * size (the fixed behaviour) rejects it. * - * - ksmbd_smb_check_perm_dacl_boundary: drives the real + * - ksmbd_smb_check_perm_dacl_boundary and + * ksmbd_smb_check_perm_dacl_maximal_boundary: drive the real * smb_check_perm_dacl() with a descriptor stored through ksmbd's own - * NTACL xattr path on a tmpfs file, and asserts that a post-boundary - * ACE is not selected for a regular access check. + * NTACL xattr path on a tmpfs file, and assert that a post-boundary + * ACE is not selected for either a regular or maximal access check. */ #include @@ -242,9 +243,49 @@ static void ksmbd_smb_check_perm_dacl_boundary_test(struct kunit *test) fput(file); } +static void +ksmbd_smb_check_perm_dacl_maximal_boundary_test(struct kunit *test) +{ + struct file *file; + struct smb_ntsd *pntsd; + __le32 daccess = FILE_MAXIMAL_ACCESS_LE; + int ntsd_size, rc; + + /* + * The in-boundary ACE grants read access. The trailing ACE grants + * write access, which must not be included in the maximal access mask. + */ + pntsd = build_boundary_ntsd(test, &test_owner_sid, FILE_READ_DATA, + FILE_WRITE_DATA, &ntsd_size); + KUNIT_ASSERT_NOT_NULL(test, pntsd); + + file = shmem_file_setup("ksmbd-kunit-dacl-maximal", 0, + mk_vma_flags(VMA_NORESERVE_BIT)); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, file); + + rc = ksmbd_vfs_set_sd_xattr(NULL, mnt_idmap(file->f_path.mnt), + &file->f_path, pntsd, ntsd_size, + false); + KUNIT_EXPECT_EQ(test, 0, rc); + if (rc) + goto out; + + rc = smb_check_perm_dacl(NULL, &file->f_path, &daccess, + FILE_MAXIMAL_ACCESS_LE, 0, false); + KUNIT_EXPECT_EQ(test, 0, rc); + if (rc) + goto out; + + KUNIT_EXPECT_TRUE(test, le32_to_cpu(daccess) & FILE_READ_DATA); + KUNIT_EXPECT_FALSE(test, le32_to_cpu(daccess) & FILE_WRITE_DATA); +out: + fput(file); +} + static struct kunit_case ksmbd_smbacl_test_cases[] = { KUNIT_CASE(ksmbd_dacl_walk_must_stop_at_declared_size), KUNIT_CASE(ksmbd_smb_check_perm_dacl_boundary_test), + KUNIT_CASE(ksmbd_smb_check_perm_dacl_maximal_boundary_test), {} }; From 6376f62089fc26b48175526ff458335c082de166 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 28 Aug 2026 16:04:45 +0900 Subject: [PATCH 16/95] ksmbd: doc: update feature status Add MSDFS, Continuous Availability (CA), and AD/DC to the ksmbd feature status list, along with resilient handle. Mark Continuous Availability, AD/DC, persistent handle, and SMB2 notify as under development. Signed-off-by: Namjae Jeon --- Documentation/filesystems/smb/ksmbd.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Documentation/filesystems/smb/ksmbd.rst b/Documentation/filesystems/smb/ksmbd.rst index 2425f321f5b7..728cfca13205 100644 --- a/Documentation/filesystems/smb/ksmbd.rst +++ b/Documentation/filesystems/smb/ksmbd.rst @@ -85,6 +85,9 @@ SMB direct(RDMA) Supported. SMB3 Multi-channel Supported. Receive Side Scaling mode Supported. SMB3.1.1 POSIX extension Supported. +MSDFS Planned for future. +Continuous Availability (CA) Under development. +AD/DC Under development. ACLs Partially Supported. only DACLs available, SACLs (auditing) is planned for the future. For ownership (SIDs) ksmbd generates random subauth @@ -97,8 +100,9 @@ ACLs Partially Supported. only DACLs available, SACLs member. Kerberos Supported. Durable handle v1,v2 Supported. -Persistent handle Planned for future. -SMB2 notify Planned for future. +Persistent handle Under development. +Resilient handle Planned for future. +SMB2 notify Under development. Sparse file support Supported. DCE/RPC support Partially Supported. a few calls(NetShareEnumAll, NetServerGetInfo, SAMR, LSARPC) that are needed From b4a07ab5d33f02ae1d953ecd51fbb4156f3eaaee Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Mon, 14 Sep 2026 01:26:35 +0200 Subject: [PATCH 17/95] ksmbd: copy stream content when renaming to a new stream name smb2_rename() creates the target stream with ksmbd_vfs_setxattr(..., NULL, 0, ...), so the source stream's content is not carried over and renaming a named stream produces a zero-length stream. Read the source stream and write it under the new name. The lookup is case-insensitive because smb2_set_stream_name_xattr() stores fp->stream.name with the client's casing, which need not be the casing on disk. A rename to the stream the handle already refers to does nothing; that comparison is case-insensitive too, since every lookup in this path is. A read failure now fails the rename instead of creating an empty target, and a write failure reports its own errno rather than -EINVAL, since writing a value can fail where writing an empty one could not. Initialize s_type, which parse_stream_name() assigns only when the name carries an explicit stream type and which selects the target xattr name. parse_stream_name() leaves the stream name empty when the stream part is, as in "::$DATA" or ":", returning NULL for the first spelling and "" for the second. Return without creating an xattr in either case, after the base name check so that a base ending in '/' is covered too. These previously created xattrs named "(null)" and "". The source xattr is left in place and fp->stream.name still refers to it, as before this change: readers of that pointer take no lock, so it cannot be swapped here. Signed-off-by: Gael Blivet Assisted-by: Claude:claude-opus-5 Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 35 ++++++++++++++++++++++++++++++----- fs/smb/server/vfs.c | 6 +++--- fs/smb/server/vfs.h | 3 +++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 0a069ac560e2..f46a29ce118f 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8022,9 +8022,11 @@ static int smb2_rename(struct ksmbd_work *work, return PTR_ERR(new_name); if (fp->is_posix_ctxt == false && strchr(new_name, ':')) { - int s_type; + int s_type = 0; char *xattr_stream_name, *stream_name = NULL; + char *stream_buf = NULL; size_t xattr_stream_size; + ssize_t stream_buf_len = 0; int len; rc = parse_stream_name(new_name, &stream_name, &s_type); @@ -8038,6 +8040,10 @@ static int smb2_rename(struct ksmbd_work *work, goto out; } + /* An empty stream name is the base file's default stream. */ + if (!stream_name || !stream_name[0]) + goto out; + rc = ksmbd_vfs_xattr_stream_name(stream_name, &xattr_stream_name, &xattr_stream_size, @@ -8045,15 +8051,34 @@ static int smb2_rename(struct ksmbd_work *work, if (rc) goto out; + /* A handle opened without a stream has no source to copy. */ + if (ksmbd_stream_fd(fp)) { + if (!strcasecmp(xattr_stream_name, fp->stream.name)) { + kfree(xattr_stream_name); + goto out; + } + + stream_buf_len = ksmbd_vfs_getcasexattr(file_mnt_idmap(fp->filp), + fp->filp->f_path.dentry, + fp->stream.name, + fp->stream.size, + &stream_buf); + if (stream_buf_len < 0) { + rc = stream_buf_len; + kfree(xattr_stream_name); + goto out; + } + } + rc = ksmbd_vfs_setxattr(file_mnt_idmap(fp->filp), &fp->filp->f_path, xattr_stream_name, - NULL, 0, 0, true); - if (rc < 0) { + stream_buf, stream_buf_len, 0, true); + kfree(stream_buf); + if (rc < 0) pr_err("failed to store stream name in xattr: %d\n", rc); - rc = -EINVAL; - } + kfree(xattr_stream_name); goto out; } diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 3bc9ef455faf..0b03e0eec0a8 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -217,9 +217,9 @@ int ksmbd_vfs_mkdir(struct ksmbd_work *work, const char *name, umode_t mode) return err; } -static ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap, - struct dentry *dentry, char *attr_name, - int attr_name_len, char **attr_value) +ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap, + struct dentry *dentry, char *attr_name, + int attr_name_len, char **attr_value) { char *name, *xattr_list = NULL; ssize_t value_len = -ENOENT, xattr_list_len; diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 67cc48e583c8..12f3ce79b3c5 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -108,6 +108,9 @@ ssize_t ksmbd_vfs_getxattr(const struct mnt_idmap *idmap, struct dentry *dentry, char *xattr_name, char **xattr_buf); +ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap, + struct dentry *dentry, char *attr_name, + int attr_name_len, char **attr_value); ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap, struct dentry *dentry, char *attr_name, int attr_name_len); From 10ea20d6584ecdaf2f7505855412a856a88d8934 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Mon, 14 Sep 2026 01:26:36 +0200 Subject: [PATCH 18/95] ksmbd: look up stream size by exact xattr name get_file_stream_info() walks the xattr list from a single ksmbd_vfs_listxattr() and sizes each stream it finds with ksmbd_vfs_casexattr_len(), which matches with strncasecmp(). Linux xattr names are case-sensitive, so two streams differing only in case report each other's size. The loop already holds the exact name, so query it directly with ksmbd_vfs_xattr_len(), which is what casexattr_len() calls once its search succeeds. Make it non-static and declare it in vfs.h, and drop the comment above it, which describes a different bug and no longer matches the code. This also drops a full listing per stream: casexattr_len() runs its own listxattr on every call, so enumerating a file with N xattrs cost N listings instead of one. Fixes: 689f1eb3719d ("ksmbd: report actual xattr value length in stream enumeration") Signed-off-by: Gael Blivet Assisted-by: Claude:claude-opus-5 Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 13 ++----------- fs/smb/server/vfs.c | 4 ++-- fs/smb/server/vfs.h | 2 ++ 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f46a29ce118f..10bcd724343c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6948,17 +6948,8 @@ static int get_file_stream_info(struct ksmbd_work *work, streamlen *= 2; kfree(stream_buf); file_info->StreamNameLength = cpu_to_le32(streamlen); - /* - * 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); + slen = ksmbd_vfs_xattr_len(file_mnt_idmap(fp->filp), + path->dentry, stream_name); ssize = slen < 0 ? 0 : (loff_t)slen; file_info->StreamSize = cpu_to_le64(ssize); file_info->StreamAllocationSize = cpu_to_le64(ssize); diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 0b03e0eec0a8..2e2c554bc2c1 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -842,8 +842,8 @@ ssize_t ksmbd_vfs_listxattr(struct dentry *dentry, char **list) return size; } -static ssize_t ksmbd_vfs_xattr_len(const struct mnt_idmap *idmap, - struct dentry *dentry, char *xattr_name) +ssize_t ksmbd_vfs_xattr_len(const struct mnt_idmap *idmap, + struct dentry *dentry, char *xattr_name) { return vfs_getxattr(idmap, dentry, xattr_name, NULL, 0); } diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 12f3ce79b3c5..216c76291fc1 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -108,6 +108,8 @@ ssize_t ksmbd_vfs_getxattr(const struct mnt_idmap *idmap, struct dentry *dentry, char *xattr_name, char **xattr_buf); +ssize_t ksmbd_vfs_xattr_len(const struct mnt_idmap *idmap, + struct dentry *dentry, char *xattr_name); ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap, struct dentry *dentry, char *attr_name, int attr_name_len, char **attr_value); From d3ef5ae55a16d4b31859e7feadbb50a0cd38dfcc Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Mon, 14 Sep 2026 01:26:37 +0200 Subject: [PATCH 19/95] ksmbd: fix AFP_AfpInfo header fields in synthesized xattr smb2_set_stream_name_xattr() synthesizes a 60-byte AFP_AfpInfo xattr when a client opens a nonexistent one on a Time Machine share, so the probe is answered instead of failing with -EBADF. Its header does not match what clients write: ksmbd uses 0x00051607 and 0x00020000 for the first two fields and leaves the third zero. The user.DosStream.AFP_AfpInfo:$DATA xattr a macOS client writes begins: 00000000: 4146 5000 0000 0100 0000 0000 0000 0080 AFP............. Write those sixteen bytes: the signature, the version at offset 4 and the backup time at offset 12. The 60-byte length is already correct and the rest stays zeroed: a zero type and creator lets the client use the file extension for icon and type detection. Fixes: eaff8e924f60 ("ksmbd: synthesize empty AFP_AfpInfo xattr on first probe") Signed-off-by: Gael Blivet Assisted-by: Claude:claude-opus-5 Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 10bcd724343c..3f82bee3a018 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3433,8 +3433,9 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path, * AAPL there too. */ static const u8 afpinfo_empty[60] = { - 0x00, 0x05, 0x16, 0x07, /* magic 0x00051607 BE */ - 0x00, 0x02, 0x00, 0x00, /* version 0x00020000 BE */ + 'A', 'F', 'P', 0x00, /* signature */ + 0x00, 0x00, 0x01, 0x00, /* version */ + [15] = 0x80, /* backup time */ }; rc = ksmbd_vfs_setxattr(idmap, path, xattr_stream_name, (void *)afpinfo_empty, From cf0ef575a56993147f087d3f251fc2a04fdffbeb Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Mon, 14 Sep 2026 01:26:38 +0200 Subject: [PATCH 20/95] ksmbd: print IPv6 client addresses correctly in procfs conn->inet_addr and conn->inet6_addr share a union, and three procfs printers choose between them by testing whether inet_addr is zero. For an IPv6 peer that field aliases the first four bytes of the address, which are zero only for :: prefixed addresses, so a client with a global IPv6 address takes the IPv4 branch and is listed from those four bytes. A client at 2001:db8::1 is listed as 32.1.13.184. IPv4 mapped addresses begin with zero bytes, so the listing is correct until a native IPv6 client connects. Record the address family where the address is stored and select on that, in the client listing and in both session listings. Fixes: fe4dc5987d7d ("ksmbd: expose connection runtime state in procfs") Signed-off-by: Gael Blivet Assisted-by: Claude:claude-opus-5 Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 2 +- fs/smb/server/connection.h | 3 +++ fs/smb/server/mgmt/user_session.c | 4 ++-- fs/smb/server/transport_tcp.c | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 3f8cc4998da5..d1cc725eeb06 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -114,7 +114,7 @@ static int proc_show_clients(struct seq_file *m, void *v) sessions++; rcu_read_unlock(); #if IS_ENABLED(CONFIG_IPV6) - if (!conn->inet_addr) + if (conn->is_ipv6) seq_printf(m, "client:\t%pI6c\n", &conn->inet6_addr); else #endif diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 371f17b4f02a..2026a2afa5e4 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -68,6 +68,9 @@ struct ksmbd_conn { u8 inet6_addr[16]; #endif }; +#if IS_ENABLED(CONFIG_IPV6) + bool is_ipv6; +#endif unsigned int inet_hash; char *request_buf; struct ksmbd_transport *transport; diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 44dc3f800cd4..1321cd6c84e7 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -130,7 +130,7 @@ static int show_proc_session(struct seq_file *m, void *v) const char *name; #if IS_ENABLED(CONFIG_IPV6) - if (chan->conn->inet_addr) + if (!chan->conn->is_ipv6) seq_printf(m, "client:\t%pI4\n", &chan->conn->inet_addr); else @@ -231,7 +231,7 @@ static int show_proc_sessions(struct seq_file *m, void *v) ksmbd_user_session_get(session); #if IS_ENABLED(CONFIG_IPV6) - if (!chan->conn->inet_addr) + if (chan->conn->is_ipv6) seq_printf(m, "client:\t%pI6c\n", &chan->conn->inet6_addr); else #endif diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index e6f663acf5f8..488ca113b431 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -77,6 +77,7 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk) if (client_sk->sk->sk_family == AF_INET6) { memcpy(&conn->inet6_addr, &client_sk->sk->sk_v6_daddr, 16); conn->inet_hash = ipv6_addr_hash(&client_sk->sk->sk_v6_daddr); + conn->is_ipv6 = true; } else { conn->inet_addr = inet_sk(client_sk->sk)->inet_daddr; conn->inet_hash = ipv4_addr_hash(inet_sk(client_sk->sk)->inet_daddr); From e8486c5cfbdd07811afa4196060bafbf78ccdae2 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Tue, 15 Sep 2026 06:46:55 +0000 Subject: [PATCH 21/95] smb/server: fix trailing period in generated short names Reproducer: 1. touch /mnt/file 2. smbinfo filealternatenameinfo /mnt/file Alternate Name: FILE~9!. See MS-FSCC 2.1.5.2.1. The base filename MUST be 1-8 characters in length and MUST NOT contain a "." period character. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb_common.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index ec6ee56293ce..7dbcfa658edd 100644 --- a/fs/smb/server/smb_common.c +++ b/fs/smb/server/smb_common.c @@ -523,8 +523,6 @@ int ksmbd_populate_dot_dotdot_entries(struct ksmbd_work *work, int info_level, * @shortname: destination short filename * * Return: shortname length or 0 when source long name is '.' or '..' - * TODO: Though this function conforms the restriction of 8.3 Filename spec, - * but the result is different with Windows 7's one. need to check. */ int ksmbd_extract_shortname(struct ksmbd_conn *conn, const char *longname, char *shortname) @@ -588,7 +586,7 @@ int ksmbd_extract_shortname(struct ksmbd_conn *conn, const char *longname, if (dot_present) memcpy(out + baselen + 4, extension, 4); else - out[baselen + 4] = '\0'; + out[baselen + 3] = '\0'; smbConvertToUTF16((__le16 *)shortname, out, PATH_MAX, conn->local_nls, 0); len = strlen(out) * 2; From dc0d85aa54175421581d5120c295aff1c92f6743 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Tue, 15 Sep 2026 06:46:56 +0000 Subject: [PATCH 22/95] smb/server: align partial normalized name responses to UTF-16 See MS-FSA 2.1.5.12.30. Set AvailableNameLength to BlockAlignTruncate((OutputBufferSize - FieldOffset(FILE_NAME_INFORMATION.FileName)), 2). Link: https://lore.kernel.org/linux-cifs/ebb45fdd-76f0-4d92-9899-13ecdaea555c@chenxiaosong.com/ Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 3f82bee3a018..370c22fc5775 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -7218,7 +7219,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 fixed_len, req_output_len; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; if (test_share_config_flag(work->tcon->share_conf, @@ -7325,6 +7326,7 @@ static int smb2_get_info_file(struct ksmbd_work *work, rc = -EOPNOTSUPP; } if (!rc) { + req_output_len = le32_to_cpu(req->OutputBufferLength); fixed_len = le32_to_cpu(rsp->OutputBufferLength); switch (fileinfoclass) { case FILE_ALL_INFORMATION: @@ -7335,14 +7337,13 @@ static int smb2_get_info_file(struct ksmbd_work *work, break; case FILE_NORMALIZED_NAME_INFORMATION: fixed_len = FILE_NORMALIZED_NAME_INFORMATION_SIZE; + req_output_len = round_down(req_output_len, 2); break; case FILE_STREAM_INFORMATION: fixed_len = FILE_STREAM_INFORMATION_SIZE; break; } - rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), - fixed_len, - rsp); + rc = buffer_check_err(req_output_len, fixed_len, rsp); } ksmbd_fd_put(work, fp); From fd4e42288fabd30c4b5636a8c5a7db06394a1787 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Tue, 15 Sep 2026 06:46:57 +0000 Subject: [PATCH 23/95] smb/server: rename smb2_file_alt_name_info to smb2_file_name_info FileAlternateNameInformation and FileNormalizedNameInformation use the same structure. Link: https://lore.kernel.org/linux-cifs/8360c969-33f5-4d96-a44d-c6fcb7a8a41f@chenxiaosong.com/ Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 12 ++++++------ fs/smb/server/smb2pdu.h | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 370c22fc5775..f1bc00765564 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6779,12 +6779,12 @@ static void get_file_alternate_info(struct ksmbd_work *work, void *rsp_org) { struct ksmbd_conn *conn = work->conn; - struct smb2_file_alt_name_info *file_info; + struct smb2_file_name_info *file_info; struct dentry *dentry = fp->filp->f_path.dentry; int conv_len; spin_lock(&dentry->d_lock); - file_info = (struct smb2_file_alt_name_info *)rsp->Buffer; + file_info = (struct smb2_file_name_info *)rsp->Buffer; conv_len = ksmbd_extract_shortname(conn, dentry->d_name.name, file_info->FileName); @@ -6831,7 +6831,7 @@ 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; + struct smb2_file_name_info *file_info; char *filename, *normalized, *stream_name; int buf_free_len, conv_len, filename_len; @@ -6865,7 +6865,7 @@ static int get_file_normalized_name_info(struct ksmbd_work *work, return -EINVAL; } - file_info = (struct smb2_file_alt_name_info *)rsp->Buffer; + file_info = (struct smb2_file_name_info *)rsp->Buffer; conv_len = smbConvertToUTF16((__le16 *)file_info->FileName, normalized, filename_len, work->conn->local_nls, 0); @@ -7333,10 +7333,10 @@ static int smb2_get_info_file(struct ksmbd_work *work, fixed_len = FILE_ALL_INFORMATION_SIZE; break; case FILE_ALTERNATE_NAME_INFORMATION: - fixed_len = FILE_ALTERNATE_NAME_INFORMATION_SIZE; + fixed_len = FILE_NAME_INFORMATION_SIZE; break; case FILE_NORMALIZED_NAME_INFORMATION: - fixed_len = FILE_NORMALIZED_NAME_INFORMATION_SIZE; + fixed_len = FILE_NAME_INFORMATION_SIZE; req_output_len = round_down(req_output_len, 2); break; case FILE_STREAM_INFORMATION: diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index ca8e27f7b712..95c89087553b 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -199,7 +199,7 @@ struct file_sparse { #define FILE_INTERNAL_INFORMATION_SIZE 8 #define FILE_EA_INFORMATION_SIZE 4 #define FILE_ACCESS_INFORMATION_SIZE 4 -#define FILE_NAME_INFORMATION_SIZE 9 +#define FILE_NAME_INFORMATION_SIZE 8 #define FILE_RENAME_INFORMATION_SIZE 10 #define FILE_LINK_INFORMATION_SIZE 11 #define FILE_NAMES_INFORMATION_SIZE 12 @@ -211,8 +211,6 @@ struct file_sparse { #define FILE_ALL_INFORMATION_SIZE 104 #define FILE_ALLOCATION_INFORMATION_SIZE 19 #define FILE_END_OF_FILE_INFORMATION_SIZE 20 -#define FILE_ALTERNATE_NAME_INFORMATION_SIZE 8 -#define FILE_NORMALIZED_NAME_INFORMATION_SIZE 8 #define FILE_STREAM_INFORMATION_SIZE 32 #define FILE_PIPE_INFORMATION_SIZE 23 #define FILE_PIPE_LOCAL_INFORMATION_SIZE 24 @@ -259,7 +257,7 @@ struct smb2_file_alignment_info { __le32 AlignmentRequirement; } __packed; -struct smb2_file_alt_name_info { +struct smb2_file_name_info { __le32 FileNameLength; char FileName[]; } __packed; From ee495de59cd738892acfd13b4898c205465068e5 Mon Sep 17 00:00:00 2001 From: Omkar Chandorkar Date: Sun, 20 Sep 2026 11:01:25 +0530 Subject: [PATCH 24/95] smb: compress: relax trailing flags in chained decompression In SMB 3.1.1, clients such as Windows 11 send chained compressed write requests containing multiple payload segments (for example, None + Pattern_V1) when transferring zero-heavy or compressible files. According to MS-SMB2 section 2.2.42.2.1, the first payload header in a chained compression transform MUST have SMB2_COMPRESSION_FLAG_CHAINED (0x0001) set. Trailing payload headers in the chain correspond to subsequent payloads, where the specification defines SMB2_COMPRESSION_FLAG_NONE (0x0000). However, Windows clients do not always explicitly zero the 16-bit Flags field on subsequent payload headers during chunk construction (as described in MS-SMB2 section 3.1.4.4 step 2), leaving residual bits in the Flags field. smb_decompress_chained() currently enforces: (!first && flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE)) returning -EINVAL if any trailing payload has non-zero flags. When this occurs during large file writes (such as copying VHDX files), ksmbd breaks the connection receive loop and abruptly terminates the TCP connection with ECONNRESET (-104), causing Windows clients to fail with error 0x8007003B (ERROR_UNEXP_NET_ERR). Fix this by relaxing the flags requirement on trailing payloads in smb_decompress_chained(). Conforming chains still strictly require SMB2_COMPRESSION_FLAG_CHAINED on the first payload header, but for subsequent payloads, only reject headers that attempt to initiate an invalid nested chain (SMB2_COMPRESSION_FLAG_CHAINED). All payload lengths, decompression algorithms, and total uncompressed output sizes continue to be strictly validated against the transform's OriginalCompressedSegmentSize. Assisted-by: OpenCode:gpt-6-astra Signed-off-by: Omkar Chandorkar Signed-off-by: Namjae Jeon --- fs/smb/common/compress/compress.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fs/smb/common/compress/compress.c b/fs/smb/common/compress/compress.c index a4123c8f1c0a..4e5164b6dc33 100644 --- a/fs/smb/common/compress/compress.c +++ b/fs/smb/common/compress/compress.c @@ -130,12 +130,18 @@ static int smb_decompress_chained(__le16 alg, bool allow_chained, len = le32_to_cpu(payload->Length); /* - * CHAINED marks only the first payload. Requiring NONE on every - * later payload rejects ambiguous or independently chained data. + * Conforming chains must set CHAINED on the first payload. + * Windows 11 clients leave uninitialized residual bits in + * Flags on subsequent payload headers. Only reject trailing + * payloads that attempt to initiate an invalid nested chain. */ - if ((first && flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) || - (!first && flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE))) - return -EINVAL; + if (first) { + if (flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) + return -EINVAL; + } else { + if (flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) + return -EINVAL; + } src += SMB2_COMPRESSION_PAYLOAD_BASE_LEN; remaining -= SMB2_COMPRESSION_PAYLOAD_BASE_LEN; From 00aee99cf53b03b1777f3e21add108fd4bb56fe8 Mon Sep 17 00:00:00 2001 From: Omkar Chandorkar Date: Sun, 20 Sep 2026 11:01:26 +0530 Subject: [PATCH 25/95] ksmbd: fix FSCTL_PIPE_TRANSCEIVE file lookup and response handling Named pipe handles are tracked in sess->rpc_handle_list rather than sess->file_table.idr. The generic IOCTL file lookup therefore returns -ENOENT and STATUS_OBJECT_NAME_NOT_FOUND for valid Windows 11 FSCTL_PIPE_TRANSCEIVE requests. Validate the RPC handle under rpc_lock and bypass the regular file lookup only when the handle exists. Return STATUS_FILE_CLOSED for invalid handles. Populate response file IDs before dispatching the transceive request. Assisted-by: OpenCode:gpt-6-astra Signed-off-by: Omkar Chandorkar Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f1bc00765564..59c97a635799 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10931,9 +10931,28 @@ 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; + case FSCTL_PIPE_TRANSCEIVE: + if (!test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_PIPE)) { + ret = -EOPNOTSUPP; + goto out; + } + + /* RPC pipe handles are not in the regular file table. */ + if (has_file_id(id) && !pid) { + down_read(&work->sess->rpc_lock); + no_fileid_ioctl = + ksmbd_session_rpc_method(work->sess, id) != 0; + up_read(&work->sess->rpc_lock); + } + if (!no_fileid_ioctl) { + ret = -EBADF; + rsp->hdr.Status = STATUS_FILE_CLOSED; + goto out2; + } + break; default: break; } @@ -11092,6 +11111,8 @@ int smb2_ioctl(struct ksmbd_work *work) break; } case FSCTL_PIPE_TRANSCEIVE: + rsp->PersistentFileId = pid; + rsp->VolatileFileId = id; out_buf_len = min_t(u32, KSMBD_IPC_MAX_PAYLOAD, out_buf_len); nbytes = fsctl_pipe_transceive(work, id, out_buf_len, req, rsp); break; From 713c31a3a225b6239a37ac7fbfb394567f8b3519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Jean?= Date: Sun, 20 Sep 2026 18:15:03 +0000 Subject: [PATCH 26/95] ksmbd: fix SMB2 CREATE response buffer overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit smb2_allocate_rsp_buf() uses the MAX_CIFS_SMALL_BUFFER_SIZE (448-byte) buffer for a single SMB2_CREATE response. That buffer also holds a 4-byte length field, which only leaves 444 bytes for the SMB2 body. The AAPL response made this buffer too small. After 8f1b796ff113, a request with AAPL response contexts may need at least 456 bytes. In the 456-byte case, create_aapl_rsp_buf() is appended last, clears 128 bytes, and writes 12 bytes past the allocation. KASAN reports: BUG: KASAN: slab-out-of-bounds in create_aapl_rsp_buf+0x31/0x6e0 Write of size 128 at addr ffff88800370954c by task kworker/0:0/9 ... create_aapl_rsp_buf+0x31/0x6e0 smb2_open+0x58f9/0xef10 ... smb2_allocate_rsp_buf+0x19d/0x370 ... The buggy address is located 332 bytes inside of allocated 448-byte region [ffff888003709400, ffff8880037095c0) Reserve enough space for any fixed CREATE response KSMBD can build. Store the required allocation size in the per-dialect values table. This keeps the small buffer for other commands and avoids using the max transaction buffer for every CREATE. Fixes: 8f1b796ff113 ("ksmbd: add AAPL kAAPL_SERVER_QUERY create context support") Assisted-by: Codex:gpt-5 Signed-off-by: Jérémy Jean Signed-off-by: Namjae Jeon --- fs/smb/common/smbglob.h | 1 + fs/smb/server/smb2ops.c | 33 +++++++++++++++++++++++++++++++++ fs/smb/server/smb2pdu.c | 6 +++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/fs/smb/common/smbglob.h b/fs/smb/common/smbglob.h index d9c7e6e7af29..fdf840888062 100644 --- a/fs/smb/common/smbglob.h +++ b/fs/smb/common/smbglob.h @@ -40,6 +40,7 @@ struct smb_version_values { size_t create_disk_id_size; size_t create_posix_size; size_t create_aapl_size; + size_t create_rsp_size; }; static inline unsigned int get_rfc1002_len(void *buf) diff --git a/fs/smb/server/smb2ops.c b/fs/smb/server/smb2ops.c index 4578291fb172..747276b171bd 100644 --- a/fs/smb/server/smb2ops.c +++ b/fs/smb/server/smb2ops.c @@ -13,6 +13,35 @@ #include "server.h" #include "stats.h" +/* + * work->response_sz includes the RFC1002 length field while smb_get_msg() + * skips over it. Durable v1 and v2 response contexts are mutually exclusive, + * and POSIX CREATE contexts are only negotiated for SMB3.1.1. + */ +#define SMB2_CREATE_RSP_SIZE(lease_size, durable_size) \ + (sizeof(__be32) + offsetof(struct smb2_create_rsp, Buffer) + \ + (lease_size) + (durable_size) + \ + sizeof(struct create_mxac_rsp) + \ + sizeof(struct create_disk_id_rsp) + \ + AAPL_RSP_MAX_SIZE) + +#define SMB21_CREATE_RSP_SIZE \ + SMB2_CREATE_RSP_SIZE(sizeof(struct create_lease), \ + sizeof(struct create_durable_rsp)) + +#define SMB3_CREATE_DURABLE_RSP_SIZE \ + ((sizeof(struct create_durable_rsp) > \ + sizeof(struct create_durable_rsp_v2)) ? \ + sizeof(struct create_durable_rsp) : \ + sizeof(struct create_durable_rsp_v2)) + +#define SMB3_CREATE_RSP_SIZE \ + SMB2_CREATE_RSP_SIZE(sizeof(struct create_lease_v2), \ + SMB3_CREATE_DURABLE_RSP_SIZE) + +#define SMB311_CREATE_RSP_SIZE \ + (SMB3_CREATE_RSP_SIZE + sizeof(struct create_posix_rsp)) + static struct smb_version_values smb21_server_values = { .version_string = SMB21_VERSION_STRING, .protocol_id = SMB21_PROT_ID, @@ -38,6 +67,7 @@ static struct smb_version_values smb21_server_values = { .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, + .create_rsp_size = SMB21_CREATE_RSP_SIZE, }; static struct smb_version_values smb30_server_values = { @@ -66,6 +96,7 @@ static struct smb_version_values smb30_server_values = { .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, + .create_rsp_size = SMB3_CREATE_RSP_SIZE, }; static struct smb_version_values smb302_server_values = { @@ -94,6 +125,7 @@ static struct smb_version_values smb302_server_values = { .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, + .create_rsp_size = SMB3_CREATE_RSP_SIZE, }; static struct smb_version_values smb311_server_values = { @@ -122,6 +154,7 @@ static struct smb_version_values smb311_server_values = { .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, + .create_rsp_size = SMB311_CREATE_RSP_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 59c97a635799..45dce9c30b6b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -855,14 +855,18 @@ static void smb2_update_lock_sequence(struct ksmbd_work *work, int smb2_allocate_rsp_buf(struct ksmbd_work *work) { struct smb2_hdr *hdr = smb_get_msg(work->request_buf); + struct smb_version_values *vals = work->conn->vals; size_t small_sz = MAX_CIFS_SMALL_BUFFER_SIZE; - size_t large_sz = small_sz + work->conn->vals->max_trans_size; + size_t large_sz = small_sz + vals->max_trans_size; size_t sz = small_sz; int cmd = le16_to_cpu(hdr->Command); if (cmd == SMB2_IOCTL_HE || cmd == SMB2_QUERY_DIRECTORY_HE) sz = large_sz; + if (cmd == SMB2_CREATE_HE) + sz = max_t(size_t, sz, vals->create_rsp_size); + if (cmd == SMB2_QUERY_INFO_HE) { struct smb2_query_info_req *req; From e0be3c1aa4468484fb5e6c863577c5638cb2a1a0 Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Mon, 21 Sep 2026 09:41:27 +0800 Subject: [PATCH 27/95] smb/server: update POSIX extension specification references The POSIX extension specifications have now been published: https://smb3posix.org/ Suggested-by: Paulo Alcantara Reviewed-by: ChenXiaoSong Signed-off-by: ZhangGuoDong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.h | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index 95c89087553b..40d61745ef41 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -156,8 +156,7 @@ struct create_durable_rsp { } __packed; /* - * See POSIX-SMB2 2.2.14.2.16 - * Link: https://gitlab.com/samba-team/smb3-posix-spec/-/blob/master/smb3_posix_extensions.md + * See POSIX-SMB2 2.1.3.2.1 */ struct create_posix_rsp { struct create_context_hdr ccontext; @@ -349,6 +348,7 @@ struct create_sd_buf_req { struct smb_ntsd ntsd; } __packed; +/* See POSIX-FSCC 2.2.1 */ struct smb2_posix_info { __le32 NextEntryOffset; __u32 Ignored; @@ -362,20 +362,18 @@ struct smb2_posix_info { __le64 Inode; __le32 DeviceId; __le32 Zero; - /* beginning of POSIX Create Context Response */ + /* + * Beginning of POSIX Create Context Response + * See POSIX-SMB2 2.1.3.2.1 + */ __le32 HardLinks; __le32 ReparseTag; __le32 Mode; /* SidBuffer contain two sids (UNIX user sid(16), UNIX group sid(16)) */ u8 SidBuffer[32]; + /* End of POSIX Create Context Response */ __le32 name_len; u8 name[]; - /* - * var sized owner SID - * var sized group SID - * le32 filenamelength - * u8 filename[] - */ } __packed; /* functions */ From e8e12d53f98a55f7cab0e97c8656a8a2846185f2 Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Mon, 21 Sep 2026 09:41:29 +0800 Subject: [PATCH 28/95] smb/common: update POSIX extension specification references The POSIX extension specifications have now been published: https://smb3posix.org/ Suggested-by: Paulo Alcantara Suggested-by: Namjae Jeon Reviewed-by: ChenXiaoSong Signed-off-by: ZhangGuoDong Signed-off-by: Namjae Jeon --- fs/smb/common/fscc.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fs/smb/common/fscc.h b/fs/smb/common/fscc.h index e46d3379b779..00f09e1a3afe 100644 --- a/fs/smb/common/fscc.h +++ b/fs/smb/common/fscc.h @@ -399,7 +399,7 @@ static_assert(offsetof(struct smb2_file_rename_info, FileName) == sizeof(struct #define FS_OBJECT_ID_INFORMATION 8 /* Query, Set */ #define FS_DRIVER_PATH_INFORMATION 9 /* Query */ #define FS_SECTOR_SIZE_INFORMATION 11 /* SMB3 or later. Query */ -/* See POSIX Extensions to MS-FSCC 2.3.1.1 */ +/* See POSIX-FSCC 2.3 */ #define FS_POSIX_INFORMATION 100 /* SMB3.1.1 POSIX. Query */ /* See MS-FSCC 2.5.1 */ @@ -575,8 +575,7 @@ struct file_notify_information { } __packed; /* - * See POSIX Extensions to MS-FSCC 2.3.2.1 - * Link: https://gitlab.com/samba-team/smb3-posix-spec/-/blob/master/fscc_posix_extensions.md + * See POSIX-FSCC 2.3.1 */ typedef struct { /* For undefined recommended transfer size return -1 in that field */ From 783a7fc454991544bf92268d8f2413af1a7c7eb8 Mon Sep 17 00:00:00 2001 From: Dairui Zhang Date: Mon, 28 Sep 2026 13:20:26 +0800 Subject: [PATCH 29/95] ksmbd: fix OOB read and cross-share confusion in ksmbd_validate_name_reconnect() ksmbd_validate_name_reconnect() indexes ab_pathname[share->path_sz + 1] without checking that the path is longer than share->path_sz + 1 (d_path() fills the buffer from the end). A durable fp is not bound to its original share, so a client can preserve a file from a short-path share and reconnect it on a long-path share, making strcmp() read past the PATH_MAX buffer - with a long enough share path this oopses the kernel, and on a mapped page it also gives a byte-equality oracle on adjacent heap (the reconnect succeeds exactly when the out-of-bounds string equals the requested name). A same-share variant with the durable fp at the share root reads 1 byte past the buffer (strlen(ab_pathname) == share->path_sz). Check before comparing: the path must be longer than share->path_sz, it must start with share->path, and the next character must be '/'. The share root itself is accepted only for an empty name. Fixes: c8efcc786146 ("ksmbd: add support for durable handles v1/v2") Reported-by: Dairui Zhang Assisted-by: LLM Cc: stable@vger.kernel.org Signed-off-by: Dairui Zhang Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index a96b764c4db5..7bc95fae4ac0 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1829,9 +1829,21 @@ int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share, return -EACCES; } - if (name && strcmp(&ab_pathname[share->path_sz + 1], name)) { - ksmbd_debug(SMB, "invalid name reconnect %s\n", name); - ret = -EINVAL; + if (name) { + size_t len = strlen(ab_pathname); + + if (len == share->path_sz && !strncmp(ab_pathname, share->path, len)) { + /* the durable fp is the share root itself */ + if (name[0]) + ret = -EINVAL; + } else if (len <= share->path_sz || + strncmp(ab_pathname, share->path, share->path_sz) || + ab_pathname[share->path_sz] != '/' || + strcmp(&ab_pathname[share->path_sz + 1], name)) { + ret = -EINVAL; + } + if (ret) + ksmbd_debug(SMB, "invalid name reconnect %s\n", name); } kfree(pathname); From 798214e6fafcdcc29f5ba2661a473232b5e70cfb Mon Sep 17 00:00:00 2001 From: Dairui Zhang Date: Tue, 29 Sep 2026 22:30:00 +0800 Subject: [PATCH 30/95] ksmbd: verify transform SessionId matches the decrypted header When an encrypted request comes in, the decryption key is picked by the SessionId in the transform header, but the request is then authorized under whatever session the decrypted inner header names. Nothing ever compares the two, so on a connection with more than one session a client can get a request decrypted with one session's key and run it as another session. Encrypted requests are exempt from signing too, so a valid AEAD tag is the only proof of session identity, and it is checked against the wrong session. MS-SMB2 says the server must check that the transform SessionId matches the one in the decrypted SMB2 header and treat a mismatch as a protocol error. Add that check in __handle_ksmbd_work() after decryption, before dispatching: the message has to be at least one fixed SMB2 header, the first operation must not set SMB2_FLAGS_RELATED_OPERATIONS and its SessionId must match the transform SessionId, and every following non-related operation in a compound chain must carry the same SessionId. NextCommand offsets must be 8-byte aligned and stay inside the message; the bounds check is done by subtraction so it cannot wrap. When the decrypted payload is a compression transform, the existing decompression step runs first and the decompressed SMB2 message is checked instead, since a compression transform header carries no SessionId. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Reported-by: Dairui Zhang Assisted-by: LLM Cc: stable@vger.kernel.org Signed-off-by: Dairui Zhang Signed-off-by: Namjae Jeon --- fs/smb/server/server.c | 16 ++++++++++++ fs/smb/server/smb2pdu.c | 54 +++++++++++++++++++++++++++++++++++++++++ fs/smb/server/smb2pdu.h | 1 + 3 files changed, 71 insertions(+) diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 0827c8c51006..eb2952f2debe 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -187,6 +187,16 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, if (conn->ops->is_transform_hdr && conn->ops->is_transform_hdr(work->request_buf)) { + u64 tr_sess_id; + + if (get_rfc1002_len(work->request_buf) < + sizeof(struct smb2_transform_hdr)) { + ksmbd_conn_abort(conn); + return; + } + tr_sess_id = le64_to_cpu(((struct smb2_transform_hdr *) + smb_get_msg(work->request_buf))->SessionId); + rc = conn->ops->decrypt_req(work); if (rc < 0) { ksmbd_conn_abort(conn); @@ -215,6 +225,12 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, ksmbd_conn_abort(conn); return; } + + rc = ksmbd_check_transform_session(work, tr_sess_id); + if (rc < 0) { + 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 45dce9c30b6b..0592e2539d1d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -12256,6 +12256,60 @@ bool smb3_is_transform_hdr(void *buf) return trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM; } +/* + * The decryption key is selected by the transform SessionId, but + * the request is authorized under the session named in the + * decrypted SMB2 header. Per MS-SMB2 the two must match, so check + * the decrypted message and its compound chain before dispatch. + */ +int ksmbd_check_transform_session(struct ksmbd_work *work, u64 tr_sess_id) +{ + struct smb2_hdr *hdr = smb_get_msg(work->request_buf); + size_t msg_len = get_rfc1002_len(work->request_buf); + size_t off = 0; + bool first = true; + + if (msg_len < sizeof(*hdr)) { + pr_err_ratelimited("Decrypted message is smaller than SMB2 header\n"); + return -ECONNABORTED; + } + + for (;;) { + u64 sid = le64_to_cpu(hdr->SessionId); + u32 next; + + if (first) { + if (hdr->Flags & SMB2_FLAGS_RELATED_OPERATIONS) { + pr_err_ratelimited("RELATED_OPERATIONS set on first operation\n"); + return -ECONNABORTED; + } + if (sid != tr_sess_id) { + pr_err_ratelimited("SessionId mismatch between transform and inner header\n"); + return -ECONNABORTED; + } + first = false; + } else if (!(hdr->Flags & SMB2_FLAGS_RELATED_OPERATIONS) && + sid != tr_sess_id) { + pr_err_ratelimited("SessionId mismatch in compound chain\n"); + return -ECONNABORTED; + } + + next = le32_to_cpu(hdr->NextCommand); + if (!next) + return 0; + if (next % 8) { + pr_err_ratelimited("NextCommand %u is not 8-byte aligned\n", next); + return -ECONNABORTED; + } + if (next > msg_len - off - sizeof(*hdr)) { + pr_err_ratelimited("NextCommand %u is out of the message\n", next); + return -ECONNABORTED; + } + off += next; + hdr = (struct smb2_hdr *)((u8 *)smb_get_msg(work->request_buf) + off); + } +} + int smb3_decrypt_req(struct ksmbd_work *work) { char *buf = work->request_buf; diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index 40d61745ef41..c90a1d4e9f11 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -416,6 +416,7 @@ struct channel *lookup_chann_list(struct ksmbd_session *sess, void smb3_preauth_hash_rsp(struct ksmbd_work *work); bool smb3_is_transform_hdr(void *buf); int smb3_decrypt_req(struct ksmbd_work *work); +int ksmbd_check_transform_session(struct ksmbd_work *work, u64 tr_sess_id); int smb3_encrypt_resp(struct ksmbd_work *work); bool smb3_11_final_sess_setup_resp(struct ksmbd_work *work); int smb2_set_rsp_credits(struct ksmbd_work *work); From 0882f124ae329eefb66b00e63b9043f24c006b0e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 30 Sep 2026 07:37:38 +0900 Subject: [PATCH 31/95] ksmbd: disconnect on invalid encryption transform flags MS-SMB2 3.3.5.2.1.1 requires the Flags/EncryptionAlgorithm field in an SMB2 transform header to be 0x0001. The server must disconnect the connection when it has any other value. smb3_decrypt_req() currently ignores this field. A client with the session key can therefore send a correctly authenticated encrypted request with invalid flags and have it processed. Check the field before looking up the session key or decrypting the request. The caller aborts the connection when this check fails. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") 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 0592e2539d1d..2fb42be6f3bc 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -12326,6 +12326,12 @@ int smb3_decrypt_req(struct ksmbd_work *work) return -ECONNABORTED; } + if (tr_hdr->Flags != cpu_to_le16(TRANSFORM_FLAG_ENCRYPTED)) { + pr_err_ratelimited("Invalid transform flags 0x%04x\n", + le16_to_cpu(tr_hdr->Flags)); + return -ECONNABORTED; + } + 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) || From 68f410faedaf6ee74d81e574733c50ebcc1bd8ed Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 30 Sep 2026 20:39:53 +0900 Subject: [PATCH 32/95] ksmbd: wait for negotiation before receiving another PDU The receive thread queues a request and can read the next PDU before the negotiation worker finishes. While need_neg is set, the next PDU can cause ksmbd_init_smb_server() to replace conn->ops. A pipelined SMB2 request can therefore make the worker handle an earlier, shorter SMB1 request with SMB2 operations and read beyond its buffer. An SMB1 multi-protocol negotiate may be followed by an SMB2 NEGOTIATE that changes conn->ops again. Wait for work queued while the connection is new or needs negotiation to complete before reading another PDU. This keeps the operations stable for both stages of negotiation without changing when need_neg is cleared. Fixes: dc8289f91238 ("ksmbd: fix slab-out-of-bounds in init_smb2_rsp_hdr") Reported-by: ihopenre-eng Reviewed-by: ihopenre-eng Signed-off-by: Namjae Jeon --- fs/smb/server/server.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index eb2952f2debe..1cb995a36b21 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -398,6 +398,7 @@ static void handle_ksmbd_work(struct work_struct *wk) static int queue_ksmbd_work(struct ksmbd_conn *conn) { struct ksmbd_work *work; + bool wait_for_neg = ksmbd_conn_new(conn) || ksmbd_conn_need_negotiate(conn); int err; err = ksmbd_init_smb_server(conn); @@ -420,6 +421,10 @@ static int queue_ksmbd_work(struct ksmbd_conn *conn) conn->last_active = jiffies; INIT_WORK(&work->work, handle_ksmbd_work); ksmbd_queue_work(work); + /* Negotiation can change conn->ops, finish it before reading another PDU. */ + if (wait_for_neg) + wait_event(conn->req_running_q, + atomic_read(&conn->req_running) == 0); return 0; } From 8852052e32df965a76daecbd8cda0ecf950b54df Mon Sep 17 00:00:00 2001 From: Daijiamu <3058704216@qq.com> Date: Thu, 1 Oct 2026 23:06:14 +0800 Subject: [PATCH 33/95] ksmbd: use clear_and_wake_up_bit() directly clear_and_wake_up_bit() was introduced by commit 8236b0ae31c8 ("bdi: wake up concurrent wb_shutdown() callers.") to replace the open-coded clear_bit_unlock() + smp_mb__after_atomic() + wake_up_bit() sequence. Replace the sequence and drop the now-trivial wake_up_oplock_break() wrapper, calling the helper directly at each call site. Suggested-by: ChenXiaoSong Signed-off-by: Daijiamu <3058704216@qq.com> Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index d0f18ebf471b..4830d30d07a7 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -763,14 +763,6 @@ static bool wait_for_break_ack(struct oplock_info *opinfo) return false; } -static void wake_up_oplock_break(struct oplock_info *opinfo) -{ - clear_bit_unlock(0, &opinfo->pending_break); - /* memory barrier is needed for wake_up_bit() */ - smp_mb__after_atomic(); - wake_up_bit(&opinfo->pending_break, 0); -} - static bool oplock_break_set_ack_wait(struct oplock_info *opinfo) { bool ret = false; @@ -826,13 +818,13 @@ static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level) if (opinfo->level <= req_op_level) { if (opinfo->is_lease == false) { - wake_up_oplock_break(opinfo); + clear_and_wake_up_bit(0, &opinfo->pending_break); return 1; } if (opinfo->o_lease->state != (SMB2_LEASE_HANDLE_CACHING_LE | SMB2_LEASE_READ_CACHING_LE)) { - wake_up_oplock_break(opinfo); + clear_and_wake_up_bit(0, &opinfo->pending_break); return 1; } } @@ -1260,7 +1252,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, SMB2_LEASE_HANDLE_CACHING_LE)) { if (!oplock_break_set_ack_wait(brk_opinfo)) { atomic_dec_if_positive(&brk_opinfo->breaking_cnt); - wake_up_oplock_break(brk_opinfo); + clear_and_wake_up_bit(0, &brk_opinfo->pending_break); return -ENOENT; } } else @@ -1309,7 +1301,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, lease->state != SMB2_LEASE_NONE_LE))) goto again; - wake_up_oplock_break(brk_opinfo); + clear_and_wake_up_bit(0, &brk_opinfo->pending_break); return err; } else { err = oplock_break_pending(brk_opinfo, req_op_level); @@ -1319,7 +1311,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { if (!oplock_break_set_ack_wait(brk_opinfo)) { - wake_up_oplock_break(brk_opinfo); + clear_and_wake_up_bit(0, &brk_opinfo->pending_break); return -ENOENT; } } @@ -1342,7 +1334,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) err = -EAGAIN; - wake_up_oplock_break(brk_opinfo); + clear_and_wake_up_bit(0, &brk_opinfo->pending_break); return err; } From 2f3f959404541e3d0450db34c473f59612633431 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 2 Oct 2026 17:21:51 +0900 Subject: [PATCH 34/95] ksmbd: support FileIdAllExtdBothDirectoryInformation Windows SMB clients can request FileIdAllExtdBothDirectoryInformation (0x51) when enumerating a directory. ksmbd does not implement this format, requiring the client to retry with an older information class. Add the packed FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION wire layout from MS-FSCC section 2.4.20 and support it throughout directory entry reservation, name retrieval and response encoding. Include the reparse tag separately from EaSize and retain the file attributes when marking special files as reparse points. Use the inode number for the 64-bit file ID, as for existing directory classes. Set the 128-bit ID to zero because the VFS stat data does not provide one, as required by MS-FSCC. Return no short name and zero reserved fields and alignment padding. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 51 ++++++++++++++++++++++++++++++++++++++ fs/smb/server/smb_common.h | 22 ++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 2fb42be6f3bc..61d4a7ed9217 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5444,6 +5444,8 @@ static int readdir_info_level_struct_sz(int info_level) return sizeof(FILE_ID_FULL_DIR_INFO); case FILEID_BOTH_DIRECTORY_INFORMATION: return sizeof(struct file_id_both_directory_info); + case FileIdAllExtdBothDirectoryInformation: + return sizeof(struct file_id_all_extd_both_directory_info); case SMB_FIND_FILE_POSIX_INFO: return sizeof(struct smb2_posix_info); default: @@ -5514,6 +5516,16 @@ static int dentry_name(struct ksmbd_dir_info *d_info, int info_level) d_info->name_len = le32_to_cpu(fibdinfo->FileNameLength); return 0; } + case FileIdAllExtdBothDirectoryInformation: + { + struct file_id_all_extd_both_directory_info *info; + + info = (struct file_id_all_extd_both_directory_info *)d_info->rptr; + d_info->rptr += le32_to_cpu(info->NextEntryOffset); + d_info->name = info->FileName; + d_info->name_len = le32_to_cpu(info->FileNameLength); + return 0; + } case SMB_FIND_FILE_POSIX_INFO: { struct smb2_posix_info *posix_info; @@ -5751,6 +5763,33 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, fibdinfo->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileIdAllExtdBothDirectoryInformation: + { + struct file_id_all_extd_both_directory_info *info; + + info = (struct file_id_all_extd_both_directory_info *)kstat; + info->FileNameLength = cpu_to_le32(conv_len); + info->EaSize = 0; + info->ReparsePointTag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + if (info->ReparsePointTag) + info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE; + if (conn->is_aapl) + info->FileId = 0; + else + info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino); + /* The VFS does not provide a 128-bit file ID. */ + memset(info->FileId128, 0, sizeof(info->FileId128)); + info->ShortNameLength = 0; + info->Reserved1 = 0; + memset(info->ShortName, 0, sizeof(info->ShortName)); + if (d_info->hide_dot_file && d_info->name[0] == '.') + info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; + memcpy(info->FileName, conv_name, conv_len); + memset(info->FileName + conv_len, 0, d_info->last_entry_off_align); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case SMB_FIND_FILE_POSIX_INFO: { struct smb2_posix_info *posix_info; @@ -5997,6 +6036,17 @@ static int reserve_populate_dentry(struct ksmbd_dir_info *d_info, fibdinfo->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileIdAllExtdBothDirectoryInformation: + { + struct file_id_all_extd_both_directory_info *info; + + info = (struct file_id_all_extd_both_directory_info *)d_info->wptr; + memcpy(info->FileName, d_info->name, d_info->name_len); + info->FileName[d_info->name_len] = 0x00; + info->FileNameLength = cpu_to_le32(d_info->name_len); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case SMB_FIND_FILE_POSIX_INFO: { struct smb2_posix_info *posix_info; @@ -6058,6 +6108,7 @@ static int verify_info_level(int info_level) case FILEID_FULL_DIRECTORY_INFORMATION: case FILEID_BOTH_DIRECTORY_INFORMATION: case SMB_FIND_FILE_POSIX_INFO: + case FileIdAllExtdBothDirectoryInformation: break; default: return -EOPNOTSUPP; diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h index 7b9c5cfcb63b..6ccfdee3ab19 100644 --- a/fs/smb/server/smb_common.h +++ b/fs/smb/server/smb_common.h @@ -133,6 +133,28 @@ struct file_id_both_directory_info { char FileName[]; } __packed; +/* MS-FSCC 2.4.20 FileIdAllExtdBothDirectoryInformation */ +struct file_id_all_extd_both_directory_info { + __le32 NextEntryOffset; + __u32 FileIndex; + __le64 CreationTime; + __le64 LastAccessTime; + __le64 LastWriteTime; + __le64 ChangeTime; + __le64 EndOfFile; + __le64 AllocationSize; + __le32 ExtFileAttributes; + __le32 FileNameLength; + __le32 EaSize; + __le32 ReparsePointTag; + __le64 FileId; + __u8 FileId128[16]; + __u8 ShortNameLength; + __u8 Reserved1; + __u8 ShortName[24]; + char FileName[]; +} __packed; + struct smb_version_ops { u16 (*get_cmd_val)(struct ksmbd_work *swork); void (*inc_reqs)(unsigned int cmd, __le32 status); From 12a6df7573763c0905383195e40c0aa53f5bfec3 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 2 Oct 2026 18:55:43 +0900 Subject: [PATCH 35/95] ksmbd: support FileIdExtdDirectoryInformation Handle FileIdExtdDirectoryInformation (0x3c) in SMB2 QUERY_DIRECTORY. Add the packed wire layout specified by MS-FSCC 2.4.23 and support this class in entry reservation, filename lookup and response encoding. Return reparse tags separately from the EA size and set the reparse flag without changing other file attributes. Zero the 128-bit file ID because the VFS provides only a 64-bit inode number, as permitted by the specification, and initialize entry padding. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 44 ++++++++++++++++++++++++++++++++++++++ fs/smb/server/smb_common.h | 18 ++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 61d4a7ed9217..32cf72097955 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5444,6 +5444,8 @@ static int readdir_info_level_struct_sz(int info_level) return sizeof(FILE_ID_FULL_DIR_INFO); case FILEID_BOTH_DIRECTORY_INFORMATION: return sizeof(struct file_id_both_directory_info); + case FILE_ID_EXTD_DIRECTORY_INFORMATION: + return sizeof(struct file_id_extd_directory_info); case FileIdAllExtdBothDirectoryInformation: return sizeof(struct file_id_all_extd_both_directory_info); case SMB_FIND_FILE_POSIX_INFO: @@ -5516,6 +5518,16 @@ static int dentry_name(struct ksmbd_dir_info *d_info, int info_level) d_info->name_len = le32_to_cpu(fibdinfo->FileNameLength); return 0; } + case FILE_ID_EXTD_DIRECTORY_INFORMATION: + { + struct file_id_extd_directory_info *info; + + info = (struct file_id_extd_directory_info *)d_info->rptr; + d_info->rptr += le32_to_cpu(info->NextEntryOffset); + d_info->name = info->FileName; + d_info->name_len = le32_to_cpu(info->FileNameLength); + return 0; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -5763,6 +5775,26 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, fibdinfo->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FILE_ID_EXTD_DIRECTORY_INFORMATION: + { + struct file_id_extd_directory_info *info; + + info = (struct file_id_extd_directory_info *)kstat; + info->FileNameLength = cpu_to_le32(conv_len); + info->EaSize = 0; + info->ReparsePointTag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + if (info->ReparsePointTag) + info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE; + /* The VFS does not provide a 128-bit file ID. */ + memset(info->FileId, 0, sizeof(info->FileId)); + if (d_info->hide_dot_file && d_info->name[0] == '.') + info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; + memcpy(info->FileName, conv_name, conv_len); + memset(info->FileName + conv_len, 0, d_info->last_entry_off_align); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6036,6 +6068,17 @@ static int reserve_populate_dentry(struct ksmbd_dir_info *d_info, fibdinfo->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FILE_ID_EXTD_DIRECTORY_INFORMATION: + { + struct file_id_extd_directory_info *info; + + info = (struct file_id_extd_directory_info *)d_info->wptr; + memcpy(info->FileName, d_info->name, d_info->name_len); + info->FileName[d_info->name_len] = 0x00; + info->FileNameLength = cpu_to_le32(d_info->name_len); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6108,6 +6151,7 @@ static int verify_info_level(int info_level) case FILEID_FULL_DIRECTORY_INFORMATION: case FILEID_BOTH_DIRECTORY_INFORMATION: case SMB_FIND_FILE_POSIX_INFO: + case FILE_ID_EXTD_DIRECTORY_INFORMATION: case FileIdAllExtdBothDirectoryInformation: break; default: diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h index 6ccfdee3ab19..458255684f6c 100644 --- a/fs/smb/server/smb_common.h +++ b/fs/smb/server/smb_common.h @@ -133,6 +133,24 @@ struct file_id_both_directory_info { char FileName[]; } __packed; +/* MS-FSCC 2.4.23 FileIdExtdDirectoryInformation */ +struct file_id_extd_directory_info { + __le32 NextEntryOffset; + __u32 FileIndex; + __le64 CreationTime; + __le64 LastAccessTime; + __le64 LastWriteTime; + __le64 ChangeTime; + __le64 EndOfFile; + __le64 AllocationSize; + __le32 ExtFileAttributes; + __le32 FileNameLength; + __le32 EaSize; + __le32 ReparsePointTag; + __u8 FileId[16]; + char FileName[]; +} __packed; + /* MS-FSCC 2.4.20 FileIdAllExtdBothDirectoryInformation */ struct file_id_all_extd_both_directory_info { __le32 NextEntryOffset; From e4ea0fef4a559167b05a8f00b567440ab2ee8732 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 2 Oct 2026 18:55:43 +0900 Subject: [PATCH 36/95] ksmbd: support FileId64ExtdDirectoryInformation Handle FileId64ExtdDirectoryInformation (0x4e) in SMB2 QUERY_DIRECTORY. Add the packed wire layout specified by MS-FSCC 2.4.19 and support this class in entry reservation, filename lookup and response encoding. Use the VFS inode number for the 64-bit file ID and retain the existing Apple extension behavior of returning a zero ID. Report reparse tags separately from the EA size, set the reparse flag without changing other file attributes, and initialize entry padding. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 46 ++++++++++++++++++++++++++++++++++++++ fs/smb/server/smb_common.h | 18 +++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 32cf72097955..39804aaf989e 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5446,6 +5446,8 @@ static int readdir_info_level_struct_sz(int info_level) return sizeof(struct file_id_both_directory_info); case FILE_ID_EXTD_DIRECTORY_INFORMATION: return sizeof(struct file_id_extd_directory_info); + case FileId64ExtdDirectoryInformation: + return sizeof(struct file_id_64_extd_directory_info); case FileIdAllExtdBothDirectoryInformation: return sizeof(struct file_id_all_extd_both_directory_info); case SMB_FIND_FILE_POSIX_INFO: @@ -5528,6 +5530,16 @@ static int dentry_name(struct ksmbd_dir_info *d_info, int info_level) d_info->name_len = le32_to_cpu(info->FileNameLength); return 0; } + case FileId64ExtdDirectoryInformation: + { + struct file_id_64_extd_directory_info *info; + + info = (struct file_id_64_extd_directory_info *)d_info->rptr; + d_info->rptr += le32_to_cpu(info->NextEntryOffset); + d_info->name = info->FileName; + d_info->name_len = le32_to_cpu(info->FileNameLength); + return 0; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -5795,6 +5807,28 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, info->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileId64ExtdDirectoryInformation: + { + struct file_id_64_extd_directory_info *info; + + info = (struct file_id_64_extd_directory_info *)kstat; + info->FileNameLength = cpu_to_le32(conv_len); + info->EaSize = 0; + info->ReparsePointTag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + if (info->ReparsePointTag) + info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE; + if (conn->is_aapl) + info->FileId = 0; + else + info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino); + if (d_info->hide_dot_file && d_info->name[0] == '.') + info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; + memcpy(info->FileName, conv_name, conv_len); + memset(info->FileName + conv_len, 0, d_info->last_entry_off_align); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6079,6 +6113,17 @@ static int reserve_populate_dentry(struct ksmbd_dir_info *d_info, info->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileId64ExtdDirectoryInformation: + { + struct file_id_64_extd_directory_info *info; + + info = (struct file_id_64_extd_directory_info *)d_info->wptr; + memcpy(info->FileName, d_info->name, d_info->name_len); + info->FileName[d_info->name_len] = 0x00; + info->FileNameLength = cpu_to_le32(d_info->name_len); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6152,6 +6197,7 @@ static int verify_info_level(int info_level) case FILEID_BOTH_DIRECTORY_INFORMATION: case SMB_FIND_FILE_POSIX_INFO: case FILE_ID_EXTD_DIRECTORY_INFORMATION: + case FileId64ExtdDirectoryInformation: case FileIdAllExtdBothDirectoryInformation: break; default: diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h index 458255684f6c..bfd0122d972c 100644 --- a/fs/smb/server/smb_common.h +++ b/fs/smb/server/smb_common.h @@ -151,6 +151,24 @@ struct file_id_extd_directory_info { char FileName[]; } __packed; +/* MS-FSCC 2.4.19 FileId64ExtdDirectoryInformation */ +struct file_id_64_extd_directory_info { + __le32 NextEntryOffset; + __u32 FileIndex; + __le64 CreationTime; + __le64 LastAccessTime; + __le64 LastWriteTime; + __le64 ChangeTime; + __le64 EndOfFile; + __le64 AllocationSize; + __le32 ExtFileAttributes; + __le32 FileNameLength; + __le32 EaSize; + __le32 ReparsePointTag; + __le64 FileId; + char FileName[]; +} __packed; + /* MS-FSCC 2.4.20 FileIdAllExtdBothDirectoryInformation */ struct file_id_all_extd_both_directory_info { __le32 NextEntryOffset; From a8787dc1bf2c2c686769a30f8c89fefb6e3f3cfc Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 2 Oct 2026 18:55:43 +0900 Subject: [PATCH 37/95] ksmbd: support FileId64ExtdBothDirectoryInformation Handle FileId64ExtdBothDirectoryInformation (0x4f) in SMB2 QUERY_DIRECTORY. Add the packed wire layout specified by MS-FSCC 2.4.18 and support this class in entry reservation, filename lookup and response encoding. Use the VFS inode number for the 64-bit file ID and retain the existing Apple extension behavior of returning a zero ID. Return an empty short name and zero its storage and the reserved byte. Report reparse tags separately from the EA size, set the reparse flag without changing other file attributes, and initialize entry padding. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 49 ++++++++++++++++++++++++++++++++++++++ fs/smb/server/smb_common.h | 21 ++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 39804aaf989e..d5bdc18e7535 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5448,6 +5448,8 @@ static int readdir_info_level_struct_sz(int info_level) return sizeof(struct file_id_extd_directory_info); case FileId64ExtdDirectoryInformation: return sizeof(struct file_id_64_extd_directory_info); + case FileId64ExtdBothDirectoryInformation: + return sizeof(struct file_id_64_extd_both_directory_info); case FileIdAllExtdBothDirectoryInformation: return sizeof(struct file_id_all_extd_both_directory_info); case SMB_FIND_FILE_POSIX_INFO: @@ -5540,6 +5542,16 @@ static int dentry_name(struct ksmbd_dir_info *d_info, int info_level) d_info->name_len = le32_to_cpu(info->FileNameLength); return 0; } + case FileId64ExtdBothDirectoryInformation: + { + struct file_id_64_extd_both_directory_info *info; + + info = (struct file_id_64_extd_both_directory_info *)d_info->rptr; + d_info->rptr += le32_to_cpu(info->NextEntryOffset); + d_info->name = info->FileName; + d_info->name_len = le32_to_cpu(info->FileNameLength); + return 0; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -5829,6 +5841,31 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, info->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileId64ExtdBothDirectoryInformation: + { + struct file_id_64_extd_both_directory_info *info; + + info = (struct file_id_64_extd_both_directory_info *)kstat; + info->FileNameLength = cpu_to_le32(conv_len); + info->EaSize = 0; + info->ReparsePointTag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + if (info->ReparsePointTag) + info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE; + if (conn->is_aapl) + info->FileId = 0; + else + info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino); + info->ShortNameLength = 0; + info->Reserved1 = 0; + memset(info->ShortName, 0, sizeof(info->ShortName)); + if (d_info->hide_dot_file && d_info->name[0] == '.') + info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; + memcpy(info->FileName, conv_name, conv_len); + memset(info->FileName + conv_len, 0, d_info->last_entry_off_align); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6124,6 +6161,17 @@ static int reserve_populate_dentry(struct ksmbd_dir_info *d_info, info->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileId64ExtdBothDirectoryInformation: + { + struct file_id_64_extd_both_directory_info *info; + + info = (struct file_id_64_extd_both_directory_info *)d_info->wptr; + memcpy(info->FileName, d_info->name, d_info->name_len); + info->FileName[d_info->name_len] = 0x00; + info->FileNameLength = cpu_to_le32(d_info->name_len); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6198,6 +6246,7 @@ static int verify_info_level(int info_level) case SMB_FIND_FILE_POSIX_INFO: case FILE_ID_EXTD_DIRECTORY_INFORMATION: case FileId64ExtdDirectoryInformation: + case FileId64ExtdBothDirectoryInformation: case FileIdAllExtdBothDirectoryInformation: break; default: diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h index bfd0122d972c..571b0e9e35d2 100644 --- a/fs/smb/server/smb_common.h +++ b/fs/smb/server/smb_common.h @@ -169,6 +169,27 @@ struct file_id_64_extd_directory_info { char FileName[]; } __packed; +/* MS-FSCC 2.4.18 FileId64ExtdBothDirectoryInformation */ +struct file_id_64_extd_both_directory_info { + __le32 NextEntryOffset; + __u32 FileIndex; + __le64 CreationTime; + __le64 LastAccessTime; + __le64 LastWriteTime; + __le64 ChangeTime; + __le64 EndOfFile; + __le64 AllocationSize; + __le32 ExtFileAttributes; + __le32 FileNameLength; + __le32 EaSize; + __le32 ReparsePointTag; + __le64 FileId; + __u8 ShortNameLength; + __u8 Reserved1; + __u8 ShortName[24]; + char FileName[]; +} __packed; + /* MS-FSCC 2.4.20 FileIdAllExtdBothDirectoryInformation */ struct file_id_all_extd_both_directory_info { __le32 NextEntryOffset; From 01ad9e65fb5231488b3ef1eae3e64a4d2a575cae Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 2 Oct 2026 18:55:43 +0900 Subject: [PATCH 38/95] ksmbd: support FileIdAllExtdDirectoryInformation Handle FileIdAllExtdDirectoryInformation (0x50) in SMB2 QUERY_DIRECTORY. Add the packed wire layout specified by MS-FSCC 2.4.21 and support this class in entry reservation, filename lookup and response encoding. Use the VFS inode number for the 64-bit file ID and retain the existing Apple extension behavior of returning a zero ID. Zero the 128-bit file ID because the VFS provides only a 64-bit inode number, as permitted by the specification. Report reparse tags separately from the EA size, set the reparse flag without changing other file attributes, and initialize entry padding. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 48 ++++++++++++++++++++++++++++++++++++++ fs/smb/server/smb_common.h | 19 +++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d5bdc18e7535..76207c4a049c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5450,6 +5450,8 @@ static int readdir_info_level_struct_sz(int info_level) return sizeof(struct file_id_64_extd_directory_info); case FileId64ExtdBothDirectoryInformation: return sizeof(struct file_id_64_extd_both_directory_info); + case FileIdAllExtdDirectoryInformation: + return sizeof(struct file_id_all_extd_directory_info); case FileIdAllExtdBothDirectoryInformation: return sizeof(struct file_id_all_extd_both_directory_info); case SMB_FIND_FILE_POSIX_INFO: @@ -5552,6 +5554,16 @@ static int dentry_name(struct ksmbd_dir_info *d_info, int info_level) d_info->name_len = le32_to_cpu(info->FileNameLength); return 0; } + case FileIdAllExtdDirectoryInformation: + { + struct file_id_all_extd_directory_info *info; + + info = (struct file_id_all_extd_directory_info *)d_info->rptr; + d_info->rptr += le32_to_cpu(info->NextEntryOffset); + d_info->name = info->FileName; + d_info->name_len = le32_to_cpu(info->FileNameLength); + return 0; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -5866,6 +5878,30 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, info->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileIdAllExtdDirectoryInformation: + { + struct file_id_all_extd_directory_info *info; + + info = (struct file_id_all_extd_directory_info *)kstat; + info->FileNameLength = cpu_to_le32(conv_len); + info->EaSize = 0; + info->ReparsePointTag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + if (info->ReparsePointTag) + info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE; + if (conn->is_aapl) + info->FileId = 0; + else + info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino); + /* The VFS does not provide a 128-bit file ID. */ + memset(info->FileId128, 0, sizeof(info->FileId128)); + if (d_info->hide_dot_file && d_info->name[0] == '.') + info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; + memcpy(info->FileName, conv_name, conv_len); + memset(info->FileName + conv_len, 0, d_info->last_entry_off_align); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6172,6 +6208,17 @@ static int reserve_populate_dentry(struct ksmbd_dir_info *d_info, info->NextEntryOffset = cpu_to_le32(next_entry_offset); break; } + case FileIdAllExtdDirectoryInformation: + { + struct file_id_all_extd_directory_info *info; + + info = (struct file_id_all_extd_directory_info *)d_info->wptr; + memcpy(info->FileName, d_info->name, d_info->name_len); + info->FileName[d_info->name_len] = 0x00; + info->FileNameLength = cpu_to_le32(d_info->name_len); + info->NextEntryOffset = cpu_to_le32(next_entry_offset); + break; + } case FileIdAllExtdBothDirectoryInformation: { struct file_id_all_extd_both_directory_info *info; @@ -6247,6 +6294,7 @@ static int verify_info_level(int info_level) case FILE_ID_EXTD_DIRECTORY_INFORMATION: case FileId64ExtdDirectoryInformation: case FileId64ExtdBothDirectoryInformation: + case FileIdAllExtdDirectoryInformation: case FileIdAllExtdBothDirectoryInformation: break; default: diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h index 571b0e9e35d2..a9cc09e31721 100644 --- a/fs/smb/server/smb_common.h +++ b/fs/smb/server/smb_common.h @@ -190,6 +190,25 @@ struct file_id_64_extd_both_directory_info { char FileName[]; } __packed; +/* MS-FSCC 2.4.21 FileIdAllExtdDirectoryInformation */ +struct file_id_all_extd_directory_info { + __le32 NextEntryOffset; + __u32 FileIndex; + __le64 CreationTime; + __le64 LastAccessTime; + __le64 LastWriteTime; + __le64 ChangeTime; + __le64 EndOfFile; + __le64 AllocationSize; + __le32 ExtFileAttributes; + __le32 FileNameLength; + __le32 EaSize; + __le32 ReparsePointTag; + __le64 FileId; + __u8 FileId128[16]; + char FileName[]; +} __packed; + /* MS-FSCC 2.4.20 FileIdAllExtdBothDirectoryInformation */ struct file_id_all_extd_both_directory_info { __le32 NextEntryOffset; From f622fedabbb58c8006ee21373968ae07bf8a9467 Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Sat, 3 Oct 2026 01:29:38 +0200 Subject: [PATCH 39/95] ksmbd: use u8 for SMB2 oplock level values The SMB2 oplock level is an unsigned 8-bit wire value, but smb20_oplock_break_ack() stores it in a plain char. On architectures where plain char is signed, the value 0xff becomes -1 and cannot compare equal to SMB2_OPLOCK_LEVEL_LEASE (0xff). The acknowledgment is consequently rejected with STATUS_INVALID_OPLOCK_PROTOCOL instead of the intended STATUS_INVALID_PARAMETER. Use u8 for the request and response oplock-level variables. This preserves all wire values in their original representation. Fixes: 80a56d4a826c ("ksmbd: align SMB2 oplock break ack handling") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Karl Mehltretter 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 76207c4a049c..a400f9912896 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -11781,7 +11781,7 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) __le32 status = STATUS_SUCCESS; int ret; u64 volatile_id, persistent_id; - char req_oplevel = 0, rsp_oplevel = 0; + u8 req_oplevel = 0, rsp_oplevel = 0; WORK_BUFFERS(work, req, rsp); From 2a90ba573d41efa82e075ba9ac532bb3ec0519c4 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 4 Oct 2026 09:58:34 +0900 Subject: [PATCH 40/95] ksmbd: cache the resolved share path for durable reconnects A share configured as /a/link/b, with /a/link pointing to /real, is opened relative to share->vfs_path at /real/b. However, ksmbd_validate_name_reconnect() compares the file's d_path() result against the original /a/link/b string and rejects a valid reconnect. Render share->vfs_path with d_path() after the existing kern_path() lookup and cache a copy in real_path along with its length. Use the resolved string for durable reconnect name validation. Keep path and path_sz as the configured spelling for existing consumers, including the volume serial calculation and path lookups. Fixes: c8efcc786146 ("ksmbd: add support for durable handles v1/v2") Reported-by: Dairui Zhang Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/share_config.c | 43 +++++++++++++++++++++++++++++-- fs/smb/server/mgmt/share_config.h | 2 ++ fs/smb/server/vfs_cache.c | 12 +++++---- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/mgmt/share_config.c b/fs/smb/server/mgmt/share_config.c index cc9f18ede80d..f85134c38cdd 100644 --- a/fs/smb/server/mgmt/share_config.c +++ b/fs/smb/server/mgmt/share_config.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -108,6 +109,7 @@ static void kill_share(struct ksmbd_share_config *share) path_put(&share->vfs_path); kfree(share->name); kfree(share->path); + kfree(share->real_path); kfree(share); } @@ -182,6 +184,43 @@ static int parse_veto_list(struct ksmbd_share_config *share, return 0; } +static int share_config_resolve_path(struct ksmbd_share_config *share) +{ + char *buf, *path; + int ret; + + ret = kern_path(share->path, 0, &share->vfs_path); + if (ret) + return ret; + + buf = kmalloc(PATH_MAX, KSMBD_DEFAULT_GFP); + if (!buf) { + ret = -ENOMEM; + goto out; + } + + path = d_path(&share->vfs_path, buf, PATH_MAX); + if (IS_ERR(path)) { + ret = PTR_ERR(path); + goto out_buf; + } + + path = kstrdup(path, KSMBD_DEFAULT_GFP); + if (!path) { + ret = -ENOMEM; + goto out_buf; + } + + share->real_path = path; + share->real_path_sz = strlen(path); +out_buf: + kfree(buf); +out: + if (ret) + path_put(&share->vfs_path); + return ret; +} + static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, const char *name) { @@ -272,12 +311,12 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, } scoped_with_init_fs() - ret = kern_path(share->path, 0, &share->vfs_path); + ret = share_config_resolve_path(share); ksmbd_revert_fsids(work); if (ret) { ksmbd_debug(SMB, "failed to access '%s'\n", share->path); - /* Avoid put_path() */ + /* No path reference is retained on failure. */ kfree(share->path); share->path = NULL; } diff --git a/fs/smb/server/mgmt/share_config.h b/fs/smb/server/mgmt/share_config.h index d157545fe7d1..91691b525907 100644 --- a/fs/smb/server/mgmt/share_config.h +++ b/fs/smb/server/mgmt/share_config.h @@ -16,8 +16,10 @@ struct ksmbd_work; struct ksmbd_share_config { char *name; char *path; + char *real_path; unsigned int path_sz; + unsigned int real_path_sz; unsigned int flags; struct list_head veto_list; diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 7bc95fae4ac0..38afc5a3799d 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1816,6 +1816,8 @@ void ksmbd_free_global_file_table(void) int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share, struct ksmbd_file *fp, char *name) { + const char *share_path = share->real_path; + size_t share_path_sz = share->real_path_sz; char *pathname, *ab_pathname; int ret = 0; @@ -1832,14 +1834,14 @@ int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share, if (name) { size_t len = strlen(ab_pathname); - if (len == share->path_sz && !strncmp(ab_pathname, share->path, len)) { + if (len == share_path_sz && !strncmp(ab_pathname, share_path, len)) { /* the durable fp is the share root itself */ if (name[0]) ret = -EINVAL; - } else if (len <= share->path_sz || - strncmp(ab_pathname, share->path, share->path_sz) || - ab_pathname[share->path_sz] != '/' || - strcmp(&ab_pathname[share->path_sz + 1], name)) { + } else if (len <= share_path_sz || + strncmp(ab_pathname, share_path, share_path_sz) || + ab_pathname[share_path_sz] != '/' || + strcmp(&ab_pathname[share_path_sz + 1], name)) { ret = -EINVAL; } if (ret) From 25e36df831a47597c03c6239fbc71285e7d714c2 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 4 Oct 2026 09:58:56 +0900 Subject: [PATCH 41/95] ksmbd: fix durable reconnects for root shares When a share exports "/", ksmbd_validate_name_reconnect() expects another slash after the share path. For a file at /foo, this rejects the valid reconnect name "foo" even though the file is in the share. Compare the requested name with the rendered file path after its leading slash when the resolved share root is "/". Keep the exact-root/empty-name check and reject rendered paths without an initial slash. Fixes: c8efcc786146 ("ksmbd: add support for durable handles v1/v2") Reported-by: Dairui Zhang Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 38afc5a3799d..ae30ee08e5e8 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1838,6 +1838,10 @@ int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share, /* the durable fp is the share root itself */ if (name[0]) ret = -EINVAL; + } else if (share_path_sz == 1 && share_path[0] == '/') { + if (ab_pathname[0] != '/' || + strcmp(ab_pathname + 1, name)) + ret = -EINVAL; } else if (len <= share_path_sz || strncmp(ab_pathname, share_path, share_path_sz) || ab_pathname[share_path_sz] != '/' || From 6868f857514bbfd2ed10b9b566664dc4a8ca039d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 4 Oct 2026 09:42:27 +0900 Subject: [PATCH 42/95] ksmbd: fix extended session security negotiation for raw NTLMSSP ksmbd_build_ntlmssp_challenge_blob() only advertises extended session security when conn->use_spnego is true. As a result, raw NTLMSSP negotiation drops NTLMSSP_NEGOTIATE_EXTENDED_SEC from the challenge even when the client requested it. A Windows capture shows an initial SPNEGO authentication succeeding, followed by a raw NTLMSSP negotiation on the same TCP connection. The raw challenge omits extended session security, and the client does not send an AUTHENTICATE message. The unfinished session then occupies the pending session slot, causing subsequent SESSION_SETUP requests to fail with STATUS_INSUFFICIENT_RESOURCES. Set NTLMSSP_NEGOTIATE_EXTENDED_SEC whenever the client requests it, independently of the SPNEGO wrapper, as required by [MS-NLMP]. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Reported-by: Tea La Forgia Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index 1b24eb3f3998..88953315b160 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -336,8 +336,7 @@ ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob, if (cflags & NTLMSSP_REQUEST_TARGET) flags |= NTLMSSP_REQUEST_TARGET; - if (conn->use_spnego && - (cflags & NTLMSSP_NEGOTIATE_EXTENDED_SEC)) + if (cflags & NTLMSSP_NEGOTIATE_EXTENDED_SEC) flags |= NTLMSSP_NEGOTIATE_EXTENDED_SEC; if (cflags & NTLMSSP_NEGOTIATE_KEY_XCH) From 02165c8d402392278a5cd2cf97645ed73a7ad4a1 Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Wed, 7 Oct 2026 22:13:51 +0900 Subject: [PATCH 43/95] ksmbd: fix named stream write and EOF handling An SMB WRITE to an existing named stream can silently discard data after its written range. ksmbd stores streams as xattr values, but the write path passes offset + count as the length of the replacement xattr even when the old stream is longer. Writing 26 bytes at offset 0 to a 60-byte stream reports 26 bytes written while discarding the remaining 34 bytes. Keep the existing xattr length when an in-place write ends before EOF. Reject writes that cross XATTR_SIZE_MAX rather than storing only the portion that fits while reporting the full requested count. Commit 4ea0bb8aaedf ("ksmbd: handle set/get info file for streamed file") intentionally skipped ordinary inode truncation for stream handles: an EOF SetInfo on an ADS had truncated the base file. That fixed the base file corruption but left stream EOF changes reporting success without resizing the stream. Handle these requests by resizing only the stream xattr; the base file size remains untouched. Zero-fill stream extensions. This is needed alongside the write fix: a short write's accidental truncation previously masked an ignored explicit EOF change. The stream resize helper returns xattr errors directly rather than using the base-file truncate path's EBADF conversion. Fixes: f44158485826 ("cifsd: add file operations") Fixes: 4ea0bb8aaedf ("ksmbd: handle set/get info file for streamed file") Cc: stable@vger.kernel.org Signed-off-by: DaeMyung Kang Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 5 +-- fs/smb/server/vfs.c | 72 ++++++++++++++++++++++++++++++++++++----- fs/smb/server/vfs.h | 1 + 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a400f9912896..0fb146b612f0 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8578,6 +8578,8 @@ static int set_end_of_file_info(struct ksmbd_work *work, struct ksmbd_file *fp, newsize = le64_to_cpu(file_eof_info->EndOfFile); inode = file_inode(fp->filp); + if (ksmbd_stream_fd(fp)) + return ksmbd_vfs_stream_truncate(fp, newsize); /* * If FILE_END_OF_FILE_INFORMATION of set_info_file is called @@ -8586,8 +8588,7 @@ static int set_end_of_file_info(struct ksmbd_work *work, struct ksmbd_file *fp, * truncate of some filesystem like FAT32 fill zero data in * truncated range. */ - if (inode->i_sb->s_magic != MSDOS_SUPER_MAGIC && - ksmbd_stream_fd(fp) == false) { + if (inode->i_sb->s_magic != MSDOS_SUPER_MAGIC) { ksmbd_debug(SMB, "truncated to newsize %lld\n", newsize); rc = ksmbd_vfs_truncate(work, fp, newsize); if (rc) { diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 2e2c554bc2c1..8bb93196acb9 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -388,23 +388,21 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, const struct cred *saved_cred; char *stream_buf = NULL, *wbuf; const struct mnt_idmap *idmap = file_mnt_idmap(fp->filp); - size_t size; + size_t size, write_end; ssize_t v_len; int err = 0; ksmbd_debug(VFS, "write stream data pos : %llu, count : %zd\n", *pos, count); - if (*pos >= XATTR_SIZE_MAX) { + if (*pos < 0 || *pos >= XATTR_SIZE_MAX) { pr_err("stream write position %lld is out of bounds\n", *pos); return -EINVAL; } - size = *pos + count; - if (size > XATTR_SIZE_MAX) { - size = XATTR_SIZE_MAX; - count = XATTR_SIZE_MAX - *pos; - } + if (count > XATTR_SIZE_MAX - *pos) + return -EFBIG; + write_end = *pos + count; saved_cred = override_creds(fp->filp->f_cred); v_len = ksmbd_vfs_getcasexattr(idmap, @@ -417,6 +415,8 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, err = v_len; goto out_revert; } + /* Preserve the tail of an existing stream on an in-place write. */ + size = max_t(size_t, v_len, write_end); if (v_len < size) { wbuf = kvzalloc(size, KSMBD_DEFAULT_GFP); @@ -445,13 +445,69 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, if (err < 0) goto out; else - fp->stream.pos = size; + fp->stream.pos = write_end; err = 0; out: kvfree(stream_buf); return err; } +/** + * ksmbd_vfs_stream_truncate() - change the length of a named stream + * @fp: stream file handle + * @newsize: new stream length + * + * Resize the stream xattr without changing the base inode size. + * + * Return: 0 on success, otherwise a negative error code + */ +int ksmbd_vfs_stream_truncate(struct ksmbd_file *fp, loff_t newsize) +{ + const struct cred *saved_cred; + const struct mnt_idmap *idmap = file_mnt_idmap(fp->filp); + char *stream_buf = NULL, *new_buf = NULL; + ssize_t v_len; + int err; + + if (newsize < 0) + return -EINVAL; + if (newsize > XATTR_SIZE_MAX) + return -EFBIG; + + saved_cred = override_creds(fp->filp->f_cred); + v_len = ksmbd_vfs_getcasexattr(idmap, fp->filp->f_path.dentry, + fp->stream.name, fp->stream.size, + &stream_buf); + if (v_len < 0) { + err = v_len; + goto out; + } + if (v_len == newsize) { + err = 0; + goto out; + } + + new_buf = stream_buf; + if (newsize > v_len) { + new_buf = kvzalloc(newsize, KSMBD_DEFAULT_GFP); + if (!new_buf) { + err = -ENOMEM; + goto out; + } + if (v_len) + memcpy(new_buf, stream_buf, v_len); + } + + err = ksmbd_vfs_setxattr(idmap, &fp->filp->f_path, fp->stream.name, + new_buf, newsize, 0, true); + if (new_buf != stream_buf) + kvfree(new_buf); +out: + kvfree(stream_buf); + revert_creds(saved_cred); + return err; +} + /** * ksmbd_vfs_write() - vfs helper for smb file write * @work: work diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 216c76291fc1..7e545d13a3ff 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -83,6 +83,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count, int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp, char *buf, size_t count, loff_t *pos, bool sync, ssize_t *written); +int ksmbd_vfs_stream_truncate(struct ksmbd_file *fp, loff_t newsize); int ksmbd_vfs_fsync(struct ksmbd_work *work, u64 fid, u64 p_id); int ksmbd_vfs_remove_file(struct ksmbd_work *work, const struct path *path); int ksmbd_vfs_link(struct ksmbd_work *work, From e0386cd6498b9c63f8c6d10b3e08670b92c0f3f1 Mon Sep 17 00:00:00 2001 From: Sung Byeongchan Date: Thu, 8 Oct 2026 17:24:44 +0900 Subject: [PATCH 44/95] ksmbd: validate AppInstanceId takeover target AppInstanceId lookup currently selects the first open with the matching identifier. CREATE for an unrelated share or path can therefore force-close another session's open before authorization of the new target. Matching only the base path also conflates alternate data streams with the unnamed stream. MS-SMB2 requires the old open to match the share and path, belong to a different ClientGuid, and be closed only when the requester has GENERIC_READ access. Search all opens with the ID and match share, resolved path, stream, and the current connection's client GUID. Retain a share configuration reference in ksmbd_file and reuse its name instead of allocating a name for every open. This keeps matching safe during tree connection teardown and preserves name-based matching across configuration updates. Refresh the share reference on durable reconnect and restore the old reference if reopening fails. Serialize connection and share comparisons with durable disconnect/reconnect, and recheck the target before closing it. Keep the original ClientGuid for durable replay and lease checks. Check AppInstanceVersion before querying access, even when GENERIC_READ is absent, and before disconnected delete-on-close cleanup. Rejected CREATE must leave other durable opens untouched. Calculate maximal access on the matched open's referenced path so a rename cannot substitute another file's authorization. Require read access with the effective VFS credentials as well as the NT DACL before takeover. Access denial only skips takeover and leaves CREATE to its normal access checks. Restore filesystem credentials before returning a takeover error. Handle takeover before checking FILE_CREATE collisions, and re-resolve the path after closing the old open. This lets delete-on-close remove the old file before a new one is created. Include SYNCHRONIZE in the baseline maximal-access mask so read-only files report GENERIC_READ correctly. Fixes: 16c30649709d ("ksmbd: handle durable v2 app instance id") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Sung Byeongchan Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 125 +++++++++++++++++++++++++++++++++----- fs/smb/server/vfs.c | 5 +- fs/smb/server/vfs_cache.c | 79 +++++++++++++++++------- fs/smb/server/vfs_cache.h | 10 ++- 4 files changed, 177 insertions(+), 42 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 0fb146b612f0..0154c0deaf8b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4119,18 +4119,76 @@ static int parse_app_instance_version(struct smb2_create_req *req, return 0; } -static int smb2_handle_app_instance_id(struct smb2_create_rsp *rsp, - struct durable_info *dh_info) +static int smb2_app_instance_stream_matches(struct ksmbd_file *fp, + char *stream_name, int s_type, + bool *matches) { - struct ksmbd_file *old_fp; - bool reject = false; + char *xattr_stream_name; + size_t xattr_stream_size; + int rc; + if (!stream_name) { + *matches = !ksmbd_stream_fd(fp); + return 0; + } + + rc = ksmbd_vfs_xattr_stream_name(stream_name, &xattr_stream_name, + &xattr_stream_size, s_type); + if (rc) + return rc; + + *matches = fp->stream.name && fp->stream.size >= 0 && + (size_t)fp->stream.size == xattr_stream_size && + !strncasecmp(fp->stream.name, xattr_stream_name, + xattr_stream_size); + kfree(xattr_stream_name); + return 0; +} + +static int smb2_handle_app_instance_id(struct ksmbd_work *work, + struct smb2_create_rsp *rsp, + struct durable_info *dh_info, + const struct path *path, + char *stream_name, int s_type, + bool *closed) +{ + struct ksmbd_conn *conn = work->conn; + struct ksmbd_file *old_fp; + const struct path *old_path; + char *app_instance_id; + __le32 maximal_access = FILE_MAXIMAL_ACCESS_LE; + bool reject = false, stream_matches; + int cursor = 0, rc; + + *closed = false; if (!dh_info->app_instance_id) return 0; + app_instance_id = dh_info->AppInstanceId; - old_fp = ksmbd_lookup_fd_app_instance_id(dh_info->AppInstanceId); - if (!old_fp) - return 0; + while (true) { + old_fp = ksmbd_lookup_fd_app_instance_id_next(app_instance_id, + &cursor); + if (!old_fp) + return 0; + + if (!ksmbd_app_instance_id_matches(work, old_fp) || + !path_equal(&old_fp->filp->f_path, path)) { + ksmbd_put_durable_fd(old_fp); + continue; + } + + rc = smb2_app_instance_stream_matches(old_fp, stream_name, + s_type, &stream_matches); + if (rc) { + ksmbd_put_durable_fd(old_fp); + return rc; + } + if (!stream_matches) { + ksmbd_put_durable_fd(old_fp); + continue; + } + break; + } if (dh_info->app_instance_version_valid) { if (old_fp->app_instance_version_valid && @@ -4145,13 +4203,34 @@ static int smb2_handle_app_instance_id(struct smb2_create_rsp *rsp, reject = true; } - ksmbd_put_durable_fd(old_fp); if (reject) { + ksmbd_put_durable_fd(old_fp); rsp->hdr.Status = STATUS_FILE_FORCED_CLOSED; return -EIO; } - return ksmbd_close_fd_app_instance_id(dh_info->AppInstanceId); + /* Authorize the referenced open even if its name changes meanwhile. */ + old_path = &old_fp->filp->f_path; + rc = smb_check_perm_dacl(conn, old_path, &maximal_access, 0, + work->sess->user->uid, false); + if (rc) { + /* Access denial prevents takeover, not the CREATE itself. */ + ksmbd_put_durable_fd(old_fp); + return rc == -EACCES ? 0 : rc; + } + if (maximal_access == FILE_MAXIMAL_ACCESS_LE) + ksmbd_vfs_query_maximal_access(mnt_idmap(old_path->mnt), + old_path->dentry, &maximal_access); + /* Also require read access with the effective VFS credentials. */ + if ((le32_to_cpu(maximal_access) & GENERIC_READ_FLAGS) != + GENERIC_READ_FLAGS || + inode_permission(mnt_idmap(old_path->mnt), + d_inode(old_path->dentry), MAY_OPEN | MAY_READ)) { + ksmbd_put_durable_fd(old_fp); + return 0; + } + + return ksmbd_close_fd_app_instance_id(work, old_fp, closed); } /** @@ -4191,6 +4270,7 @@ int smb2_open(struct ksmbd_work *work) char *name = NULL; char *stream_name = NULL; bool file_present = false, created = false, already_permitted = false; + bool app_instance_id_checked = false, app_instance_closed; int share_ret, need_truncate = 0; u64 time, alloc_size = 0; umode_t posix_mode = 0; @@ -4365,13 +4445,6 @@ 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)); @@ -4484,9 +4557,29 @@ int smb2_open(struct ksmbd_work *work) goto err_out2; } +resolve_path: + file_present = false; + maximal_access = 0; rc = ksmbd_vfs_kern_path(work, name, LOOKUP_NO_SYMLINKS, &path, 1); + /* Validate takeover before disconnected delete-on-close cleanup. */ + if (!rc && dh_info.app_instance_id && !dh_info.reconnected && + !dh_info.replay && !app_instance_id_checked) { + app_instance_id_checked = true; + rc = smb2_handle_app_instance_id(work, rsp, &dh_info, &path, + stream_name, s_type, + &app_instance_closed); + if (rc) { + path_put(&path); + goto err_out1; + } + if (app_instance_closed) { + path_put(&path); + goto resolve_path; + } + } + /* * A durable handle opened with delete-on-close is preserved across a * disconnect so it can be reclaimed by a durable reconnect. When a new diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 8bb93196acb9..a6230ed64e11 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -119,10 +119,11 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf, void ksmbd_vfs_query_maximal_access(const struct mnt_idmap *idmap, struct dentry *dentry, __le32 *daccess) { - *daccess = cpu_to_le32(FILE_READ_ATTRIBUTES | READ_CONTROL); + *daccess = cpu_to_le32(FILE_READ_ATTRIBUTES | READ_CONTROL | + SYNCHRONIZE); if (!inode_permission(idmap, d_inode(dentry), MAY_OPEN | MAY_WRITE)) - *daccess |= cpu_to_le32(WRITE_DAC | WRITE_OWNER | SYNCHRONIZE | + *daccess |= cpu_to_le32(WRITE_DAC | WRITE_OWNER | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES | FILE_DELETE_CHILD); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index ae30ee08e5e8..496055ce4a4d 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -664,6 +664,7 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) if (ksmbd_stream_fd(fp)) kfree(fp->stream.name); + ksmbd_share_config_put(fp->share_conf); kfree(fp->owner.name); kmem_cache_free(filp_cache, fp); @@ -962,37 +963,54 @@ bool ksmbd_has_other_active_fd(struct ksmbd_file *fp) return ret; } -struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id) +bool ksmbd_app_instance_id_matches(struct ksmbd_work *work, + struct ksmbd_file *fp) { - struct ksmbd_file *fp = NULL; - unsigned int id; + bool matches; + + /* Keep the current connection and share alive across the comparison. */ + spin_lock(&fp->f_lock); + matches = READ_ONCE(fp->f_state) == FP_INITED && + fp->conn && fp->share_conf && + !strcmp(fp->share_conf->name, work->tcon->share_conf->name) && + memcmp(fp->conn->ClientGUID, work->conn->ClientGUID, + SMB2_CLIENT_GUID_SIZE); + spin_unlock(&fp->f_lock); + + return matches; +} + +struct ksmbd_file * +ksmbd_lookup_fd_app_instance_id_next(char *app_instance_id, int *cursor) +{ + struct ksmbd_file *fp, *found = NULL; + int id = *cursor; 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, + while ((fp = idr_get_next(global_ft.idr, &id))) { + *cursor = id + 1; + if (fp->has_app_instance_id && + !memcmp(fp->app_instance_id, app_instance_id, SMB2_CREATE_GUID_SIZE)) { - fp = ksmbd_fp_get(fp); - break; + found = ksmbd_fp_get(fp); + if (found) + break; } + id++; } read_unlock(&global_ft.lock); - return fp; + return found; } -int ksmbd_close_fd_app_instance_id(char *app_instance_id) +int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work, + struct ksmbd_file *fp, bool *closed) { struct ksmbd_file_table *ft; - struct ksmbd_file *fp; struct oplock_info *opinfo; int n_to_drop = 0; - fp = ksmbd_lookup_fd_app_instance_id(app_instance_id); - if (!fp) - return 0; - + *closed = false; opinfo = opinfo_get(fp); if (!opinfo) goto out; @@ -1005,7 +1023,8 @@ int ksmbd_close_fd_app_instance_id(char *app_instance_id) ft = &opinfo->sess->file_table; write_lock(&ft->lock); - if (fp->f_state == FP_INITED && has_file_id(fp->volatile_id)) { + if (fp->f_state == FP_INITED && has_file_id(fp->volatile_id) && + ksmbd_app_instance_id_matches(work, fp)) { idr_remove(ft->idr, fp->volatile_id); fp->volatile_id = KSMBD_NO_FID; n_to_drop = ksmbd_mark_fp_closed(fp); @@ -1022,6 +1041,7 @@ int ksmbd_close_fd_app_instance_id(char *app_instance_id) list_del_init(&fp->node); up_write(&fp->f_ci->m_lock); + *closed = true; if (atomic_sub_and_test(n_to_drop, &fp->refcount)) { if (fp->conn) atomic_dec(&fp->conn->stats.open_files_count); @@ -1233,6 +1253,8 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) */ fp->conn = ksmbd_conn_get(work->conn); fp->tcon = work->tcon; + fp->share_conf = work->tcon->share_conf; + atomic_inc(&fp->share_conf->refcount); fp->volatile_id = KSMBD_NO_FID; fp->persistent_id = KSMBD_NO_FID; fp->f_state = FP_NEW; @@ -1255,6 +1277,7 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) err_out: /* fp->conn was set and refcounted before every branch here. */ ksmbd_conn_put(fp->conn); + ksmbd_share_config_put(fp->share_conf); kmem_cache_free(filp_cache, fp); return ERR_PTR(ret); } @@ -1759,9 +1782,11 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, ksmbd_conn_put(lock_conn); } + spin_lock(&fp->f_lock); fp->conn = NULL; fp->tcon = NULL; fp->volatile_id = KSMBD_NO_FID; + spin_unlock(&fp->f_lock); if (fp->durable_timeout) fp->durable_scavenger_timeout = @@ -1862,6 +1887,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) struct ksmbd_inode *ci; struct oplock_info *op; struct ksmbd_conn *conn = work->conn; + struct ksmbd_share_config *old_share; struct ksmbd_lock *smb_lock; unsigned int old_f_state; @@ -1886,21 +1912,29 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) * partially-initialized fp. fp owns a strong reference on the new * conn (see ksmbd_open_fd()); undo it on __open_id() failure. */ - fp->conn = ksmbd_conn_get(conn); - fp->tcon = work->tcon; - write_unlock(&global_ft.lock); - + spin_lock(&fp->f_lock); old_f_state = fp->f_state; fp->f_state = FP_NEW; + old_share = fp->share_conf; + fp->share_conf = work->tcon->share_conf; + atomic_inc(&fp->share_conf->refcount); + fp->conn = ksmbd_conn_get(conn); + fp->tcon = work->tcon; + spin_unlock(&fp->f_lock); + write_unlock(&global_ft.lock); __open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID); if (!has_file_id(fp->volatile_id)) { write_lock(&global_ft.lock); + spin_lock(&fp->f_lock); fp->conn = NULL; fp->tcon = NULL; + fp->share_conf = old_share; + fp->f_state = old_f_state; + spin_unlock(&fp->f_lock); write_unlock(&global_ft.lock); ksmbd_conn_put(conn); - fp->f_state = old_f_state; + ksmbd_share_config_put(work->tcon->share_conf); return -EBADF; } @@ -1927,6 +1961,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) kfree(fp->owner.name); fp->owner.name = NULL; spin_unlock(&fp->f_lock); + ksmbd_share_config_put(old_share); return 0; } diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 732ae26dd6a7..706633bb0419 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -102,6 +102,8 @@ struct ksmbd_file { struct oplock_info __rcu *f_opinfo; struct ksmbd_conn *conn; struct ksmbd_tree_connect *tcon; + /* Own a share reference independently of tcon; rebind under f_lock. */ + struct ksmbd_share_config *share_conf; atomic_t refcount; __le32 daccess; @@ -207,8 +209,12 @@ 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); +bool ksmbd_app_instance_id_matches(struct ksmbd_work *work, + struct ksmbd_file *fp); +struct ksmbd_file * +ksmbd_lookup_fd_app_instance_id_next(char *app_instance_id, int *cursor); +int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work, + struct ksmbd_file *fp, bool *closed); 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); From 626611dc22a3b63b3264a7a797d9dbc530234fcd Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Fri, 9 Oct 2026 00:25:35 +0900 Subject: [PATCH 45/95] ksmbd: return end of file for reads past a named stream's data A READ on a non-empty named stream at or beyond its end fails with STATUS_INVALID_PARAMETER. ksmbd_vfs_stream_read() returns -EINVAL when the offset is not below the stream length, and smb2_read() maps -EINVAL to that status. smb2_read() already turns a zero-byte read into STATUS_END_OF_FILE, the status a regular file returns at its end, and an empty stream already takes that path. Return 0 instead of -EINVAL so that non-empty streams do the same. Clients that read a stream until end of file otherwise get an error once they have read all of its data. The buffered copychunk path converts a zero-byte source read to -EIO, as it does for a regular file that shrinks during copying. Fixes: 2ae1a6cc4302 ("cifsd: fix potential read overflow in ksmbd_vfs_stream_read()") Cc: stable@vger.kernel.org Signed-off-by: DaeMyung Kang Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index a6230ed64e11..02ee73d8d631 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -270,7 +270,7 @@ static int ksmbd_vfs_stream_read(struct ksmbd_file *fp, char *buf, loff_t *pos, return (int)v_len; if (v_len <= *pos) { - count = -EINVAL; + count = 0; goto free_buf; } From a7d1d52748e0fe4d06fdb871fc9899904fbba93f Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Fri, 9 Oct 2026 00:25:36 +0900 Subject: [PATCH 46/95] ksmbd: use the existing xattr name for a named stream SMB stream names are case-insensitive, but ksmbd stores each named stream in an xattr whose name is case-sensitive. Opening an existing stream finds its xattr with a case-insensitive match, yet the handle keeps the name as the client spelled it, and every later operation that needs an exact name uses that spelling: - a WRITE or an end-of-file change creates a second xattr, so a stream created as "Foo" and written through "foo" ends up stored twice, and which value a later lookup returns depends on xattr list order; - delete-on-close and delete-pending removal can fail with -ENODATA, or remove only a case-variant copy and leave the original; - the share mode check compares stream names with strcmp(), so two opens of the same stream that differ only in case do not conflict. Have ksmbd_vfs_casexattr_len() copy the matching xattr's name into the handle when the stream is opened, so later operations use the name already on disk. The match must have the same length as the name it replaces; the stream lookup passes a length that includes the terminating NUL, which already guarantees that, and the check makes the copy safe for any caller. A stream that does not exist yet keeps the client's spelling, as before. smb2_rename() compares and looks up fp->stream.name case-insensitively, so it behaves the same with either spelling. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Signed-off-by: DaeMyung Kang Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 8 ++++---- fs/smb/server/vfs.c | 9 +++++++-- fs/smb/server/vfs.h | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 0154c0deaf8b..79b0a9aee860 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3341,7 +3341,7 @@ static int smb2_set_ea(struct smb2_ea_info *eabuf, unsigned int buf_len, path->dentry, attr_name, XATTR_USER_PREFIX_LEN + - eabuf->EaNameLength); + eabuf->EaNameLength, NULL); /* delete the EA only when it exits */ if (rc > 0) { @@ -3413,11 +3413,11 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path, fp->stream.name = xattr_stream_name; fp->stream.size = xattr_stream_size; - /* Check if there is stream prefix in xattr space */ + /* Keep the existing xattr's case for subsequent writes and removal. */ rc = ksmbd_vfs_casexattr_len(idmap, path->dentry, xattr_stream_name, - xattr_stream_size); + xattr_stream_size, xattr_stream_name); if (rc >= 0) return 0; @@ -3469,7 +3469,7 @@ 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); + fp->stream.size, NULL); return slen < 0 ? 0 : (loff_t)slen; } diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 02ee73d8d631..eb0212e9a5f4 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1956,7 +1956,7 @@ int ksmbd_vfs_fill_dentry_attrs(struct ksmbd_work *work, ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap, struct dentry *dentry, char *attr_name, - int attr_name_len) + int attr_name_len, char *actual_name) { char *name, *xattr_list = NULL; ssize_t value_len = -ENOENT, xattr_list_len; @@ -1970,8 +1970,13 @@ ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap, ksmbd_debug(VFS, "%s, len %zd\n", name, strlen(name)); if (strncasecmp(attr_name, name, attr_name_len)) continue; + if (actual_name && strlen(name) + 1 != attr_name_len) + continue; value_len = ksmbd_vfs_xattr_len(idmap, dentry, name); + /* The caller provides attr_name_len bytes for the actual name. */ + if (value_len >= 0 && actual_name) + memcpy(actual_name, name, attr_name_len); break; } @@ -2126,7 +2131,7 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, 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); + src_fp->stream.name, src_fp->stream.size, NULL); revert_creds(saved_cred); if (src_file_size < 0) return src_file_size; diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 7e545d13a3ff..21b0d29a3db0 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -116,7 +116,7 @@ ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap, int attr_name_len, char **attr_value); ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap, struct dentry *dentry, char *attr_name, - int attr_name_len); + int attr_name_len, char *actual_name); int ksmbd_vfs_setxattr(const struct mnt_idmap *idmap, const struct path *path, const char *attr_name, void *attr_value, size_t attr_size, int flags, From 80c816c047f7cae3c99cc75740c1f5d2c4d97b45 Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Fri, 9 Oct 2026 00:25:37 +0900 Subject: [PATCH 47/95] ksmbd: preserve unreadable named streams on open A write-only client can open an existing named stream with FILE_OPEN_IF without permission to read the underlying file. The case-insensitive lookup finds the xattr name, but querying its value length then fails with -EACCES. smb2_set_stream_name_xattr() treats every negative result as a missing stream and replaces the existing xattr with an empty value. A failed xattr list is not proof that the stream is absent either: for example, -ENOMEM can lead to the same empty replacement. Preserve listing errors and create a stream only for -ENOENT or -ENODATA. The latter can occur if the xattr is removed between listing its name and querying its value. Return other lookup errors without changing the stream. A write-only FILE_OPEN now reports access denied instead of name not found. FILE_OVERWRITE_IF and FILE_SUPERSEDE on an unreadable stream also fail instead of appearing to replace it. Stream writes and EOF changes already need to read the old value, so such handles cannot update the stream after opening either. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Signed-off-by: DaeMyung Kang Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 2 ++ fs/smb/server/vfs.c | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 79b0a9aee860..b57942263e13 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3420,6 +3420,8 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path, xattr_stream_size, xattr_stream_name); if (rc >= 0) return 0; + if (rc != -ENOENT && rc != -ENODATA) + return rc; if (fp->cdoption == FILE_OPEN_LE) { if (!strcmp(stream_name, "AFP_AfpInfo") && diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index eb0212e9a5f4..d21ed90454f9 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1962,7 +1962,11 @@ ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap, ssize_t value_len = -ENOENT, xattr_list_len; xattr_list_len = ksmbd_vfs_listxattr(dentry, &xattr_list); - if (xattr_list_len <= 0) + if (xattr_list_len < 0) { + value_len = xattr_list_len; + goto out; + } + if (!xattr_list_len) goto out; for (name = xattr_list; name - xattr_list < xattr_list_len; From 4ca76b89917ad5b317e07ae4f0edc16f7c2a1248 Mon Sep 17 00:00:00 2001 From: Stefan Metzmacher Date: Fri, 9 Oct 2026 12:02:11 +0200 Subject: [PATCH 48/95] smb: smbdirect: don't wait for RDMA_CM_EVENT_DISCONNECTED in smbdirect_socket_destroy_sync() smbdirect_socket_destroy_sync() waited for RDMA_CM_EVENT_DISCONNECTED after smbdirect_socket_cleanup_work() called rdma_disconnect(). That can take very long, e.g. if the peer just disappeared, until the rdma cm gives up, or forever if the event already happened, but the status was overwritten afterwards. This is not needed: smbdirect_socket_destroy() drains the qp under rdma_lock_handler(), destroys it and calls rdma_destroy_id(), which only waits for a currently running event handler and makes sure no further events are delivered. The rest of the disconnect protocol (DREQ/DREP and timewait for IB, abrupt close for iWarp) is handled by the rdma core asynchronously. drivers/nvme/host/rdma.c also just calls rdma_disconnect() and ib_drain_qp() before rdma_destroy_id(). So smbdirect_socket_destroy() now also accepts SMBDIRECT_SOCKET_DISCONNECTING and changes the status to SMBDIRECT_SOCKET_DISCONNECTED itself under rdma_lock_handler(). We could still get RDMA_CM_EVENT_DISCONNECTED in the small windows between rdma_unlock_handler() and rdma_destroy_id(), but in that case smbdirect_connection_rdma_event_handler() is basically a no-op. Fixes: 422a2436697d ("smb: smbdirect: introduce smbdirect_socket_destroy[_sync]()") Cc: Namjae Jeon Cc: Paulo Alcantara Cc: Tom Talpey Cc: linux-cifs@vger.kernel.org Cc: samba-technical@lists.samba.org Cc: linux-rdma@vger.kernel.org Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/connection.c | 12 ++++---- fs/smb/smbdirect/socket.c | 56 ++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/fs/smb/smbdirect/connection.c b/fs/smb/smbdirect/connection.c index afd31fa12a36..f48c857bda5a 100644 --- a/fs/smb/smbdirect/connection.c +++ b/fs/smb/smbdirect/connection.c @@ -83,9 +83,9 @@ static int smbdirect_connection_rdma_event_handler(struct rdma_cm_id *id, * smbdirect_socket_schedule_cleanup[_status]() => * smbdirect_socket_cleanup_work(). * - * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING, - * but never ever get RDMA_CM_EVENT_DISCONNECTED and - * never reach SMBDIRECT_SOCKET_DISCONNECTED. + * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING + * and call rdma_disconnect(), but never ever get + * RDMA_CM_EVENT_DISCONNECTED. */ if (event->event == RDMA_CM_EVENT_DEVICE_REMOVAL) smbdirect_socket_schedule_cleanup_status(sc, @@ -113,9 +113,9 @@ static int smbdirect_connection_rdma_event_handler(struct rdma_cm_id *id, * smbdirect_socket_schedule_cleanup_status() => * smbdirect_socket_cleanup_work(). * - * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING, - * but never ever get RDMA_CM_EVENT_DISCONNECTED and - * never reach SMBDIRECT_SOCKET_DISCONNECTED. + * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING + * and call rdma_disconnect(), but never ever get + * RDMA_CM_EVENT_DISCONNECTED. * * This is also a normal disconnect so * SMBDIRECT_LOG_INFO should be good enough diff --git a/fs/smb/smbdirect/socket.c b/fs/smb/smbdirect/socket.c index bb02df6158b9..c36cb7cc0088 100644 --- a/fs/smb/smbdirect/socket.c +++ b/fs/smb/smbdirect/socket.c @@ -511,7 +511,13 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) if (sc->status == SMBDIRECT_SOCKET_DESTROYED) return; - WARN_ONCE(sc->status != SMBDIRECT_SOCKET_DISCONNECTED, + /* + * smbdirect_socket_destroy_sync() doesn't wait + * for RDMA_CM_EVENT_DISCONNECTED, so we may still + * be in SMBDIRECT_SOCKET_DISCONNECTING + * (or already reached SMBDIRECT_SOCKET_DISCONNECTED) + */ + WARN_ONCE(sc->status < SMBDIRECT_SOCKET_DISCONNECTING, "status=%s first_error=%1pe", smbdirect_socket_status_string(sc->status), SMBDIRECT_DEBUG_ERR_PTR(sc->first_error)); @@ -542,6 +548,32 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) if (sc->rdma.cm_id) rdma_lock_handler(sc->rdma.cm_id); + /* + * We hold the handler lock, so the rdma event + * handlers can't change the status anymore. + * + * If RDMA_CM_EVENT_DISCONNECTED didn't arrive yet, + * we just stop waiting for it here. + * + * We already disabled disconnect_work above + * and before we call rdma_unlock_handler below + * we call smbdirect_connection_destroy_qp which + * sets sc->ib.qp = NULL. + * + * Between rdma_unlock_handler() and + * rdma_destroy_id() below there's a small + * windows where RDMA_CM_EVENT_DISCONNECTED + * could still arrive. + * + * But smbdirect_connection_rdma_event_handler + * will be a noop when calling smbdirect_socket_schedule_cleanup* + * ib_drain_qp() also won't be called. + */ + if (sc->status < SMBDIRECT_SOCKET_DISCONNECTED) { + sc->status = SMBDIRECT_SOCKET_DISCONNECTED; + smbdirect_socket_wake_up_all(sc); + } + if (sc->ib.qp) { smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, "drain qp\n"); @@ -679,17 +711,21 @@ void smbdirect_socket_destroy_sync(struct smbdirect_socket *sc) "destroying rdma session\n"); if (sc->status < SMBDIRECT_SOCKET_DISCONNECTING) smbdirect_socket_cleanup_work(&sc->disconnect_work); - if (sc->status < SMBDIRECT_SOCKET_DISCONNECTED) { - smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, - "wait for transport being disconnected\n"); - wait_event(sc->status_wait, sc->status == SMBDIRECT_SOCKET_DISCONNECTED); - smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, - "waited for transport being disconnected\n"); - } /* - * Once we reached SMBDIRECT_SOCKET_DISCONNECTED, - * we should call smbdirect_socket_destroy() + * We don't wait for RDMA_CM_EVENT_DISCONNECTED, + * rdma_disconnect() was already called by + * smbdirect_socket_cleanup_work() if needed + * and smbdirect_socket_destroy() drains the qp, + * destroys it and calls rdma_destroy_id(), which + * only waits for a currently running event handler + * and makes sure no further events are delivered. + * The rest of the disconnect protocol is handled + * by the rdma core asynchronously. + * + * Waiting for RDMA_CM_EVENT_DISCONNECTED could + * take very long or forever, e.g. if the peer + * just disappeared. */ smbdirect_socket_destroy(sc); smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, From 328c7e307e8411899e2f90beae83da9e4c45d700 Mon Sep 17 00:00:00 2001 From: Stefan Metzmacher Date: Fri, 9 Oct 2026 12:02:12 +0200 Subject: [PATCH 49/95] smb: smbdirect: release failed pending sockets of a listener A child of a listener only left the listen.pending or listen.ready list via smbdirect_socket_accept() or when the listener was destroyed. So every connection that failed before it was accepted (negotiate timeout, peer disconnect, invalid negotiate request, ...) stayed on the list and counted against the backlog until the listener was destroyed. After about backlog (10 for ksmbd) such connections smbdirect_listen_connect_request() rejected every new connection with -EBUSY, so an unauthenticated peer was able to permanently stop ksmbd from accepting SMB-Direct connections. Now a failed child moves itself to a new listen.orphaned list at the end of smbdirect_socket_cleanup_work(), after the disconnect was started, and queues listen.purge_orphaned_work, which releases it with smbdirect_socket_release(), without holding the listener's rdma_lock_handler() lock, in the same way smbdirect_socket_destroy() does for the remaining children. Orphaned sockets still count against the backlog until they are released. sc->accept.listener is now only changed under listener->listen.lock and whoever clears it is responsible for releasing the socket: smbdirect_socket_accept(), purge_orphaned_work or the destroy of the listener. In order to make that work: - a struct smbdirect_socket that is a listener is now freed via kfree_rcu(), so that a child can dereference sc->accept.listener under rcu_read_lock(), even if the listener is released concurrently. - smbdirect_listen_connect_request() adds nsc to the listen.pending list and sets nsc->accept.listener before it calls smbdirect_accept_connect_request(), and always returns 0 so the rdma_cm core keeps the connection id and nsc owns it. On a non-zero return it schedules nsc's teardown with smbdirect_socket_schedule_cleanup(), which orphans it off the listener so that purge_orphaned_work releases it; nsc destroys its own connection id with rdma_destroy_id() in that teardown. The teardown has to be deferred because the rdma_cm core holds nsc's id_priv->handler_mutex across the handler, so nsc->rdma.cm_id must not be destroyed synchronously from here. That same handler_mutex (and the listener's) also guarantees nsc can't go away under us, so setting the listener before the call is safe and lets a failure orphan nsc. - smbdirect_accept_negotiate_recv_work() no longer moves a socket to the ready list if it already failed or no longer belongs to the listener. Every accepting socket has a listener, so if sc->accept.listener is already cleared, it no longer sends a negotiate response, as the socket is about to be released. - smbdirect_socket_destroy() of the listener disables purge_orphaned_work and also releases the orphaned sockets. Fixes: dc691b91ad16 ("smb: smbdirect: introduce smbdirect_socket_{listen,accept}()") Reported-by: Lee Seong Hyeon Closes: https://lore.kernel.org/linux-cifs/CANTrAmxL5sWU8Sn29JAGZvSq5PBB2OA+VKA0MHsZJMesagtfzA@mail.gmail.com/ Reported-by: Sashiko Closes: https://sashiko.dev/#/patchset/cover.1791224972.git.metze%40samba.org Closes: https://sashiko.dev/#/patchset/cover.1791229120.git.metze%40samba.org Cc: Namjae Jeon Cc: Paulo Alcantara Cc: Tom Talpey Cc: linux-cifs@vger.kernel.org Cc: samba-technical@lists.samba.org Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/accept.c | 143 +++++++++++++++++++++++------------- fs/smb/smbdirect/internal.h | 2 + fs/smb/smbdirect/listen.c | 137 +++++++++++++++++++++++++++++++--- fs/smb/smbdirect/socket.c | 57 +++++++++++++- fs/smb/smbdirect/socket.h | 42 +++++++++++ 5 files changed, 315 insertions(+), 66 deletions(-) diff --git a/fs/smb/smbdirect/accept.c b/fs/smb/smbdirect/accept.c index 1b30ca8476c1..977144442974 100644 --- a/fs/smb/smbdirect/accept.c +++ b/fs/smb/smbdirect/accept.c @@ -19,15 +19,33 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, const struct rdma_conn_param *param) { struct smbdirect_socket_parameters *sp = &sc->parameters; - struct smbdirect_recv_io *recv_io; + struct smbdirect_recv_io *recv_io = NULL; u8 peer_initiator_depth; u8 peer_responder_resources; struct rdma_conn_param conn_param; __be32 ird_ord_hdr[2]; int ret; - if (SMBDIRECT_CHECK_STATUS_WARN(sc, SMBDIRECT_SOCKET_CREATED)) - return -EINVAL; + /* + * Install the real event handler (and the event we expect next) + * before anything that can fail. On an early error the caller + * (smbdirect_listen_connect_request()) only schedules our deferred + * cleanup and returns 0 to the rdma_cm core, which keeps the cm_id, so + * the cm_id must already carry smbdirect_accept_rdma_event_handler(): + * it turns any unexpected event (e.g. RDMA_CM_EVENT_DEVICE_REMOVAL) + * into a clean smbdirect_socket_schedule_cleanup(). Otherwise the + * placeholder smbdirect_socket_rdma_event_handler() installed by + * smbdirect_socket_init_accepting() stays in place and returns -ESTALE, + * which makes the rdma_cm core destroy the cm_id behind the back of our + * pending teardown. + */ + sc->rdma.expected_event = RDMA_CM_EVENT_ESTABLISHED; + sc->rdma.cm_id->event_handler = smbdirect_accept_rdma_event_handler; + + if (SMBDIRECT_CHECK_STATUS_WARN(sc, SMBDIRECT_SOCKET_CREATED)) { + ret = -EINVAL; + goto cleanup; + } /* * First set what the we as server are able to support @@ -48,7 +66,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR, "smbdirect_accept_init_params() failed %1pe\n", SMBDIRECT_DEBUG_ERR_PTR(ret)); - goto init_params_failed; + goto cleanup; } ret = smbdirect_connection_create_qp(sc); @@ -56,7 +74,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR, "smbdirect_connection_create_qp() failed %1pe\n", SMBDIRECT_DEBUG_ERR_PTR(ret)); - goto create_qp_failed; + goto cleanup; } ret = smbdirect_connection_create_mem_pools(sc); @@ -64,7 +82,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR, "smbdirect_connection_create_mem_pools() failed %1pe\n", SMBDIRECT_DEBUG_ERR_PTR(ret)); - goto create_mem_failed; + goto cleanup; } recv_io = smbdirect_connection_get_recv_io(sc); @@ -73,7 +91,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR, "smbdirect_connection_get_recv_io() failed %1pe\n", SMBDIRECT_DEBUG_ERR_PTR(ret)); - goto get_recv_io_failed; + goto cleanup; } recv_io->cqe.done = smbdirect_accept_negotiate_recv_done; @@ -87,7 +105,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR, "smbdirect_connection_post_recv_io() failed %1pe\n", SMBDIRECT_DEBUG_ERR_PTR(ret)); - goto post_recv_io_failed; + goto cleanup; } /* * From here recv_io is known to the RDMA QP and needs ib_drain_qp and @@ -123,14 +141,12 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, /* explicitly set above */ WARN_ON_ONCE(sc->status != SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED); sc->status = SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING; - sc->rdma.expected_event = RDMA_CM_EVENT_ESTABLISHED; - sc->rdma.cm_id->event_handler = smbdirect_accept_rdma_event_handler; ret = rdma_accept(sc->rdma.cm_id, &conn_param); if (ret) { smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR, "rdma_accept() failed %1pe\n", SMBDIRECT_DEBUG_ERR_PTR(ret)); - goto rdma_accept_failed; + goto cleanup; } /* @@ -144,31 +160,25 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, return 0; -rdma_accept_failed: +cleanup: /* - * 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). + * No RDMA state is torn down here, and in particular sc is not + * released: the caller (smbdirect_listen_connect_request()) owns that + * decision and schedules sc's teardown via + * smbdirect_socket_schedule_cleanup() on a non-zero return. All of the + * RDMA state - the QP, the memory pools and the cm_id itself - is then + * torn down asynchronously by smbdirect_socket_cleanup_work() -> + * smbdirect_socket_release() -> smbdirect_socket_destroy(), which + * drains the QP (returning any posted recv_io to the pool) before + * destroying the memory pools and destroys the cm_id with + * rdma_destroy_id(), all in the correct order. + * + * The only resource not reachable by that teardown is a recv_io that + * was taken from the pool but not yet posted to the QP (recv_io is set + * to NULL after a successful post), so hand it back here. */ - 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: - smbdirect_connection_destroy_mem_pools(sc); -create_mem_failed: - smbdirect_connection_destroy_qp(sc); -create_qp_failed: -init_params_failed: return ret; } @@ -302,6 +312,7 @@ static void smbdirect_accept_negotiate_recv_work(struct work_struct *work) struct smbdirect_socket *sc = container_of(work, struct smbdirect_socket, connect.work); struct smbdirect_socket_parameters *sp = &sc->parameters; + struct smbdirect_socket *lsc; struct smbdirect_recv_io *recv_io; struct smbdirect_negotiate_req *nreq; unsigned long flags; @@ -464,29 +475,55 @@ static void smbdirect_accept_negotiate_recv_work(struct work_struct *work) */ sp->max_fragmented_send_size = max_fragmented_size; - if (sc->accept.listener) { - struct smbdirect_socket *lsc = sc->accept.listener; - unsigned long flags; - - spin_lock_irqsave(&lsc->listen.lock, flags); - list_del(&sc->accept.list); - list_add_tail(&sc->accept.list, &lsc->listen.ready); - wake_up(&lsc->listen.wait_queue); - spin_unlock_irqrestore(&lsc->listen.lock, flags); - - /* - * smbdirect_socket_accept() will call - * smbdirect_accept_negotiate_finish(nsc, 0); - * - * So that we don't send the negotiation - * response that grants credits to the peer - * before the socket is accepted by the - * application. - */ + /* + * Every accepting socket was created by + * smbdirect_listen_connect_request() and has a + * listener. If it's already cleared, the listener + * (or its purge_orphaned_work) is responsible for + * releasing us, so we must not send a negotiate + * response. + * + * The memory of the listener is freed via kfree_rcu(), + * see smbdirect_listen_orphan_socket(). + */ + rcu_read_lock(); + lsc = READ_ONCE(sc->accept.listener); + if (!lsc) { + rcu_read_unlock(); return; } - ntstatus = le32_to_cpu(STATUS_SUCCESS); + spin_lock_irqsave(&lsc->listen.lock, flags); + /* + * The listener (or its purge_orphaned_work) + * may have cleared sc->accept.listener in the + * meantime and is responsible for releasing us. + * + * If we already failed, we're either + * already on the orphaned list or + * smbdirect_socket_cleanup_work() will + * move us there. + * + * In both cases we must not move us + * to the ready list. + */ + if (sc->accept.listener == lsc && !READ_ONCE(sc->first_error)) { + list_move_tail(&sc->accept.list, &lsc->listen.ready); + wake_up(&lsc->listen.wait_queue); + } + spin_unlock_irqrestore(&lsc->listen.lock, flags); + rcu_read_unlock(); + + /* + * smbdirect_socket_accept() will call + * smbdirect_accept_negotiate_finish(nsc, 0); + * + * So that we don't send the negotiation + * response that grants credits to the peer + * before the socket is accepted by the + * application. + */ + return; not_supported: smbdirect_accept_negotiate_finish(sc, ntstatus); @@ -839,7 +876,7 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc, struct smbdirect_socket, accept.list); if (nsc) { - nsc->accept.listener = NULL; + WRITE_ONCE(nsc->accept.listener, NULL); list_del_init_careful(&nsc->accept.list); arg->is_empty = list_empty_careful(&lsc->listen.ready); } diff --git a/fs/smb/smbdirect/internal.h b/fs/smb/smbdirect/internal.h index e9959e6dc13a..69d0c9f899de 100644 --- a/fs/smb/smbdirect/internal.h +++ b/fs/smb/smbdirect/internal.h @@ -74,6 +74,8 @@ void __smbdirect_socket_schedule_cleanup(struct smbdirect_socket *sc, void smbdirect_socket_destroy_sync(struct smbdirect_socket *sc); +void smbdirect_listen_orphan_socket(struct smbdirect_socket *sc); + int smbdirect_socket_wait_for_credits(struct smbdirect_socket *sc, enum smbdirect_socket_status expected_status, int unexpected_errno, diff --git a/fs/smb/smbdirect/listen.c b/fs/smb/smbdirect/listen.c index 2f78bcaedbf8..3c91f00d95ce 100644 --- a/fs/smb/smbdirect/listen.c +++ b/fs/smb/smbdirect/listen.c @@ -10,6 +10,78 @@ static int smbdirect_listen_rdma_event_handler(struct rdma_cm_id *id, struct rdma_cm_event *event); +/* + * This is called by a socket that failed while it + * is still on the pending or ready list of its + * listener, typically at the end of + * smbdirect_socket_cleanup_work(), when the + * disconnect was already started. + * + * It moves itself to the orphaned list and + * lets smbdirect_listen_purge_orphaned_work() + * release it. Otherwise it would stay on the + * pending or ready list until the listener is + * destroyed and fill up the backlog, so that + * no new connections would be accepted. + * + * It's fine to call this more than once. + */ +void smbdirect_listen_orphan_socket(struct smbdirect_socket *sc) +{ + struct smbdirect_socket *lsc; + unsigned long flags; + + /* + * The memory of the listener is freed via + * kfree_rcu(), so it's safe to dereference it + * under rcu_read_lock(), even if sc->accept.listener + * is cleared and the listener is released concurrently. + */ + rcu_read_lock(); + lsc = READ_ONCE(sc->accept.listener); + if (!lsc) { + rcu_read_unlock(); + return; + } + + spin_lock_irqsave(&lsc->listen.lock, flags); + if (sc->accept.listener == lsc) { + list_move_tail(&sc->accept.list, &lsc->listen.orphaned); + queue_work(lsc->workqueues.cleanup, &lsc->listen.purge_orphaned_work); + } + spin_unlock_irqrestore(&lsc->listen.lock, flags); + rcu_read_unlock(); +} + +static void smbdirect_listen_purge_orphaned_work(struct work_struct *work) +{ + struct smbdirect_socket *lsc = + container_of(work, struct smbdirect_socket, listen.purge_orphaned_work); + struct smbdirect_socket *psc, *tsc; + LIST_HEAD(orphaned_list); + unsigned long flags; + + /* + * Clearing accept.listener under listen.lock + * makes us responsible for releasing them. + */ + spin_lock_irqsave(&lsc->listen.lock, flags); + list_splice_tail_init(&lsc->listen.orphaned, &orphaned_list); + list_for_each_entry(psc, &orphaned_list, accept.list) + WRITE_ONCE(psc->accept.listener, NULL); + spin_unlock_irqrestore(&lsc->listen.lock, flags); + + /* + * We don't hold the listener's rdma_lock_handler() + * lock here, see smbdirect_socket_destroy() + * for why that's important. + */ + list_for_each_entry_safe(psc, tsc, &orphaned_list, accept.list) { + list_del_init(&psc->accept.list); + smbdirect_socket_release(psc); + } +} + int smbdirect_socket_listen(struct smbdirect_socket *sc, int backlog) { int ret; @@ -47,6 +119,9 @@ int smbdirect_socket_listen(struct smbdirect_socket *sc, int backlog) sc->rdma.cm_id->event_handler = smbdirect_listen_rdma_event_handler; rdma_unlock_handler(sc->rdma.cm_id); + INIT_WORK(&sc->listen.purge_orphaned_work, + smbdirect_listen_purge_orphaned_work); + ret = rdma_listen(sc->rdma.cm_id, backlog); if (ret) { sc->first_error = ret; @@ -214,6 +289,7 @@ static int smbdirect_listen_connect_request(struct smbdirect_socket *lsc, size_t backlog = max_t(size_t, 1, lsc->listen.backlog); size_t psockets; size_t rsockets; + size_t osockets; int ret; if (!smbdirect_frwr_is_supported(&new_id->device->attrs)) { @@ -254,14 +330,21 @@ static int smbdirect_listen_connect_request(struct smbdirect_socket *lsc, spin_lock_irqsave(&lsc->listen.lock, flags); psockets = list_count_nodes(&lsc->listen.pending); rsockets = list_count_nodes(&lsc->listen.ready); + /* + * Orphaned sockets still count against + * the backlog until they are released by + * smbdirect_listen_purge_orphaned_work(). + */ + osockets = list_count_nodes(&lsc->listen.orphaned); spin_unlock_irqrestore(&lsc->listen.lock, flags); if (psockets > backlog || rsockets > backlog || - (psockets + rsockets) > backlog) { + osockets > backlog || + (psockets + rsockets + osockets) > backlog) { smbdirect_log_rdma_event(lsc, SMBDIRECT_LOG_ERR, - "Backlog[%d][%zu] full pending[%zu] ready[%zu]\n", - lsc->listen.backlog, backlog, psockets, rsockets); + "Backlog[%d][%zu] full pending[%zu] ready[%zu] orphaned[%zu]\n", + lsc->listen.backlog, backlog, psockets, rsockets, osockets); return -EBUSY; } @@ -279,22 +362,54 @@ static int smbdirect_listen_connect_request(struct smbdirect_socket *lsc, if (ret) goto set_settings_failed; + /* + * Publish nsc on the pending list with accept.listener set before + * smbdirect_accept_connect_request() calls rdma_accept(). Once the + * connection can establish, smbdirect_accept_negotiate_recv_work() + * may run and it must observe accept.listener, otherwise nsc would + * be left stranded on the pending list forever. + * + * The listener's handler_mutex is held while we're called, so + * smbdirect_socket_destroy() of the listener can't reach nsc on the + * pending list before we're done. + * + * From here nsc is published on the pending list: on failure of + * smbdirect_accept_connect_request() below we schedule nsc's teardown, + * which orphans nsc off the listener so + * smbdirect_listen_purge_orphaned_work() -> smbdirect_socket_release() + * releases it. + */ spin_lock_irqsave(&lsc->listen.lock, flags); list_add_tail(&nsc->accept.list, &lsc->listen.pending); - nsc->accept.listener = lsc; + WRITE_ONCE(nsc->accept.listener, lsc); spin_unlock_irqrestore(&lsc->listen.lock, flags); ret = smbdirect_accept_connect_request(nsc, &event->param.conn); - if (ret) - goto accept_connect_failed; + if (ret) { + /* + * The rdma_cm core holds both the listener's and nsc's + * id_priv->handler_mutex across this CONNECT_REQUEST handler, + * so neither can go away under us here and nsc cannot receive + * any rdma event: it is safe to hand nsc to its teardown. + * + * That teardown must be deferred though: nsc->rdma.cm_id + * (= new_id) has its handler_mutex held by us, so it cannot be + * destroyed synchronously from here. + * smbdirect_socket_schedule_cleanup() only queues work; + * smbdirect_socket_cleanup_work() then orphans nsc off the + * listener and smbdirect_listen_purge_orphaned_work() releases + * it, destroying nsc's cm_id with rdma_destroy_id(). + */ + smbdirect_socket_schedule_cleanup(nsc, ret); + } + /* + * Always return 0 so the rdma_cm core keeps new_id: nsc owns it now. + * On success it stays connected; on failure its deferred teardown + * above destroys it. + */ return 0; -accept_connect_failed: - spin_lock_irqsave(&lsc->listen.lock, flags); - list_del_init(&nsc->accept.list); - nsc->accept.listener = NULL; - spin_unlock_irqrestore(&lsc->listen.lock, flags); set_settings_failed: set_params_failed: /* diff --git a/fs/smb/smbdirect/socket.c b/fs/smb/smbdirect/socket.c index c36cb7cc0088..e270e36b3145 100644 --- a/fs/smb/smbdirect/socket.c +++ b/fs/smb/smbdirect/socket.c @@ -319,6 +319,15 @@ void __smbdirect_socket_schedule_cleanup(struct smbdirect_socket *sc, * (smbdirect_socket_destroy) to reap. */ if (sc->listen.backlog != -1) { /* was a listener */ + /* + * We don't move them to the orphaned list here, + * each child does that itself at the end of its + * smbdirect_socket_cleanup_work(), see + * smbdirect_listen_orphan_socket(). Only that checks + * accept.listener, which is still NULL for a child + * that smbdirect_listen_connect_request() is still + * setting up and will release itself on failure. + */ 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) @@ -427,6 +436,15 @@ static void smbdirect_socket_cleanup_work(struct work_struct *work) * instances of one class -- harmless, but lockdep cannot tell). */ if (sc->listen.backlog != -1) { /* was a listener */ + /* + * We don't move them to the orphaned list here, + * each child does that itself at the end of its + * smbdirect_socket_cleanup_work(), see + * smbdirect_listen_orphan_socket(). Only that checks + * accept.listener, which is still NULL for a child + * that smbdirect_listen_connect_request() is still + * setting up and will release itself on failure. + */ 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) @@ -486,6 +504,18 @@ static void smbdirect_socket_cleanup_work(struct work_struct *work) * in order to notice the broken connection. */ smbdirect_socket_wake_up_all(sc); + + /* + * If we're still on the pending or ready list + * of a listener, we started the disconnect + * as far as possible above, so we move ourself + * to the orphaned list of the listener, + * which will release us. + * + * This is a no-op internally if + * sc->accept.listener is NULL. + */ + smbdirect_listen_orphan_socket(sc); } static void smbdirect_socket_destroy(struct smbdirect_socket *sc) @@ -544,6 +574,7 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) disable_work_sync(&sc->recv_io.posted.refill_work); disable_work_sync(&sc->idle.immediate_work); disable_delayed_work_sync(&sc->idle.timer_work); + disable_work_sync(&sc->listen.purge_orphaned_work); if (sc->rdma.cm_id) rdma_lock_handler(sc->rdma.cm_id); @@ -607,6 +638,19 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) spin_lock_irqsave(&sc->listen.lock, flags); list_splice_tail_init(&sc->listen.ready, &pending_list); list_splice_tail_init(&sc->listen.pending, &pending_list); + /* + * purge_orphaned_work is already disabled above, + * so we also need to release the orphaned sockets. + * + * Clearing accept.listener under listen.lock + * makes us responsible for releasing them and + * prevents them from moving themselves to + * the orphaned list via + * smbdirect_listen_orphan_socket(). + */ + list_splice_tail_init(&sc->listen.orphaned, &pending_list); + list_for_each_entry(psc, &pending_list, accept.list) + WRITE_ONCE(psc->accept.listener, NULL); spin_unlock_irqrestore(&sc->listen.lock, flags); /* It's not possible for upper layer to get to reassembly */ @@ -650,7 +694,6 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) "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 */ @@ -777,7 +820,17 @@ static void smbdirect_socket_release_destroy(struct kref *kref) * in DESTROYED state, before we free the memory. */ smbdirect_socket_destroy_sync(sc); - kfree(sc); + + /* + * Only a listener (backlog != -1) is ever dereferenced + * via sc->accept.listener under rcu_read_lock(), see + * smbdirect_listen_orphan_socket(). Other sockets can be + * freed immediately. + */ + if (sc->listen.backlog != -1) /* was a listener */ + kfree_rcu(sc, refs.rcu); + else + kfree(sc); } void smbdirect_socket_release(struct smbdirect_socket *sc) diff --git a/fs/smb/smbdirect/socket.h b/fs/smb/smbdirect/socket.h index c09eddd8ad16..14114a2567a2 100644 --- a/fs/smb/smbdirect/socket.h +++ b/fs/smb/smbdirect/socket.h @@ -151,6 +151,12 @@ struct smbdirect_socket { * the disconnect refcount. */ struct kref destroy; + + /* + * smbdirect_socket_release_destroy() uses + * kfree_rcu(), see accept.listener. + */ + struct rcu_head rcu; } refs; /* RDMA related */ @@ -216,6 +222,16 @@ struct smbdirect_socket { * only be > 0. */ int backlog; + /* + * Sockets on pending or ready that failed + * move themselves to orphaned and queue + * purge_orphaned_work, which releases them. + * So that they are freed while the listener + * is still alive. They still count against + * the backlog until they are released. + */ + struct list_head orphaned; + struct work_struct purge_orphaned_work; } listen; /* @@ -226,7 +242,30 @@ struct smbdirect_socket { * connection. */ struct { + /* + * This is only set, protected by + * listener->listen.lock, while the socket + * is owned by the listener (on its pending, + * ready or orphaned list). The one who clears + * it is responsible for releasing the socket. + * + * The memory of a struct smbdirect_socket is + * freed via kfree_rcu(), so the listener + * can be dereferenced under rcu_read_lock(), + * even if accept.listener is cleared and + * the listener is released concurrently. + */ struct smbdirect_socket *listener; + /* + * Linkage on one of the listener's + * listen.{pending,ready,orphaned} lists. + * + * accept.list and accept.listener are set + * together under listener->listen.lock before + * smbdirect_accept_connect_request() is called, + * so whenever the socket is on one of those lists + * accept.listener is that listener (never NULL). + */ struct list_head list; } accept; @@ -588,6 +627,9 @@ static __always_inline void smbdirect_socket_init(struct smbdirect_socket *sc) spin_lock_init(&sc->listen.lock); INIT_LIST_HEAD(&sc->listen.pending); INIT_LIST_HEAD(&sc->listen.ready); + INIT_LIST_HEAD(&sc->listen.orphaned); + INIT_WORK(&sc->listen.purge_orphaned_work, __smbdirect_socket_disabled_work); + disable_work_sync(&sc->listen.purge_orphaned_work); sc->listen.backlog = -1; /* not a listener */ init_waitqueue_head(&sc->listen.wait_queue); From 5c171c90c446bdce80ebaecfc5ed2c410bcc7427 Mon Sep 17 00:00:00 2001 From: Stefan Metzmacher Date: Fri, 9 Oct 2026 12:02:13 +0200 Subject: [PATCH 50/95] smb: smbdirect: don't hand out already failed sockets in smbdirect_socket_accept() A socket on the ready list is in SMBDIRECT_SOCKET_NEGOTIATE_RUNNING, but it may fail before smbdirect_socket_accept() picks it up, e.g. RDMA_CM_EVENT_DISCONNECTED already moved it to SMBDIRECT_SOCKET_DISCONNECTED. smbdirect_socket_accept() unconditionally overwrote the status with SMBDIRECT_SOCKET_CONNECTED and handed out the already disconnected socket. smbdirect_socket_cleanup_work() then called rdma_disconnect() on it again, and before the recent commit smbdirect_socket_destroy_sync() waited forever for an RDMA_CM_EVENT_DISCONNECTED that already happened. Now we check first_error and change the status to SMBDIRECT_SOCKET_CONNECTED only via cmpxchg() from SMBDIRECT_SOCKET_NEGOTIATE_RUNNING, while we still hold listen.lock. If the socket already failed, we move it to the orphaned list and queue purge_orphaned_work, like smbdirect_listen_orphan_socket() does, and try the next one. If we only found failed sockets, we wait for the next one. Fixes: dc691b91ad16 ("smb: smbdirect: introduce smbdirect_socket_{listen,accept}()") Reported-by: Sashiko Closes: https://sashiko.dev/#/patchset/cover.1791224972.git.metze%40samba.org Closes: https://sashiko.dev/#/bug/linux-f9dbca09-30a7-4145-9bdb-4676759982c8 Cc: Namjae Jeon Cc: Paulo Alcantara Cc: Tom Talpey Cc: linux-cifs@vger.kernel.org Cc: samba-technical@lists.samba.org Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/accept.c | 84 +++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/fs/smb/smbdirect/accept.c b/fs/smb/smbdirect/accept.c index 977144442974..e7e5eb75a5ae 100644 --- a/fs/smb/smbdirect/accept.c +++ b/fs/smb/smbdirect/accept.c @@ -836,7 +836,11 @@ static long smbdirect_socket_wait_for_accept(struct smbdirect_socket *lsc, long if (ret < 0) return ret; - return 0; + /* + * Return the remaining timeout, so the caller can carry it + * over to the next smbdirect_socket_wait_for_accept() call. + */ + return ret; } struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc, @@ -846,6 +850,7 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc, struct smbdirect_socket *nsc; unsigned long flags; +again: if (lsc->status != SMBDIRECT_SOCKET_LISTENING) { arg->err = -EINVAL; return NULL; @@ -857,7 +862,7 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc, } if (list_empty_careful(&lsc->listen.ready)) { - int ret; + long ret; if (timeo == 0) { arg->err = -EAGAIN; @@ -865,16 +870,64 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc, } ret = smbdirect_socket_wait_for_accept(lsc, timeo); - if (ret) { + if (ret < 0) { arg->err = ret; return NULL; } + /* + * Carry the remaining timeout over, so that a stream of + * failed connections that we orphan and skip (goto again) + * can't reset the caller's timeout and wait forever. + */ + timeo = ret; } spin_lock_irqsave(&lsc->listen.lock, flags); - nsc = list_first_entry_or_null(&lsc->listen.ready, - struct smbdirect_socket, - accept.list); + /* + * Recheck under the lock. + */ + if (lsc->status != SMBDIRECT_SOCKET_LISTENING) { + arg->err = -EINVAL; + spin_unlock_irqrestore(&lsc->listen.lock, flags); + return NULL; + } + if (lsc->first_error) { + arg->err = lsc->first_error; + spin_unlock_irqrestore(&lsc->listen.lock, flags); + return NULL; + } + + while ((nsc = list_first_entry_or_null(&lsc->listen.ready, + struct smbdirect_socket, + accept.list))) { + /* + * nsc may have failed after it was moved + * to the ready list, e.g. RDMA_CM_EVENT_DISCONNECTED + * already moved it to SMBDIRECT_SOCKET_DISCONNECTED. + * We must not overwrite that with + * SMBDIRECT_SOCKET_CONNECTED and hand + * out an already disconnected socket. + * + * Doing this under listen.lock means nsc + * still belongs to us, so we can just move + * a failed socket to the orphaned list, + * like smbdirect_listen_orphan_socket() does, + * and try the next one. + */ + if (!READ_ONCE(nsc->first_error) && + cmpxchg(&nsc->status, + SMBDIRECT_SOCKET_NEGOTIATE_RUNNING, + SMBDIRECT_SOCKET_CONNECTED) == + SMBDIRECT_SOCKET_NEGOTIATE_RUNNING) + break; + + smbdirect_log_rdma_event(nsc, SMBDIRECT_LOG_INFO, + "orphaning failed socket status=%s first_error=%1pe\n", + smbdirect_socket_status_string(nsc->status), + SMBDIRECT_DEBUG_ERR_PTR(nsc->first_error)); + list_move_tail(&nsc->accept.list, &lsc->listen.orphaned); + queue_work(lsc->workqueues.cleanup, &lsc->listen.purge_orphaned_work); + } if (nsc) { WRITE_ONCE(nsc->accept.listener, NULL); list_del_init_careful(&nsc->accept.list); @@ -882,8 +935,13 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc, } spin_unlock_irqrestore(&lsc->listen.lock, flags); if (!nsc) { - arg->err = -EAGAIN; - return NULL; + /* + * If we only found failed sockets or no socket, + * we wait for the next one. + * + * A possible -EAGAIN/-ETIMEOUT is handled above. + */ + goto again; } /* @@ -892,12 +950,14 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc, * so it didn't grant any credits to us. * * The caller expects a connected socket - * now as there are no credits anyway. + * now as there are no credits anyway, + * above we already changed to SMBDIRECT_SOCKET_CONNECTED + * under the lsc->listen.lock and with cmpxchg. * - * Then we send the negotiation response in - * order to grant credits to the peer. + * Now we send the negotiation response in + * order to grant credits to the peer, + * as the socket is now visible to the application layer. */ - nsc->status = SMBDIRECT_SOCKET_CONNECTED; smbdirect_accept_negotiate_finish(nsc, 0); return nsc; From e8592dec2e935befe842f90f4044d210f59c4d30 Mon Sep 17 00:00:00 2001 From: Stefan Metzmacher Date: Fri, 9 Oct 2026 12:28:27 +0200 Subject: [PATCH 51/95] smb: smbdirect: don't WARN on an unexpected receive opcode The receive completion handlers smbdirect_connection_recv_io_done(), smbdirect_accept_negotiate_recv_done() and smbdirect_connect_negotiate_recv_done() verify that the completion is a regular receive with WARN_ON_ONCE(wc->opcode != IB_WC_RECV). But wc->opcode is controlled by the remote peer: a posted receive work request is consumed not only by an incoming SEND, but also by an RDMA Write with Immediate, which completes as IB_WC_RECV_RDMA_WITH_IMM. An unauthenticated peer can transmit such a message, even before the negotiate completes, so it reaches the accept/connect negotiate handlers as well. With panic_on_warn set this is a remote denial of service, and otherwise it still taints the kernel and spams the log for something a peer can trigger at will. The opcode check itself was inherited from the original client and server recv_done() handlers (fs/smb/client/smbdirect.c and fs/smb/server/transport_rdma.c), where an unexpected opcode was simply logged and the connection was torn down. Only the WARN_ON_ONCE() was added on top during the socketization rework. Drop the WARN_ON_ONCE() and keep the existing graceful error path, which logs the unexpected opcode (unless the status is IB_WC_WR_FLUSH_ERR) and tears the connection down. Fixes: ea4151222a93 ("smb: smbdirect: introduce smbdirect_connection_recv_io_done()") Fixes: eb3ed1e9048c ("smb: smbdirect: introduce smbdirect_accept_connect_request()") Fixes: d1f187656797 ("smb: smbdirect: introduce smbdirect_connect[_sync]()") Reported-by: Sashiko Closes: https://sashiko.dev/#/bug/linux-5341a0c0-7b00-4eef-915c-91e099649d3e Cc: Namjae Jeon Cc: Paulo Alcantara Cc: Tom Talpey Cc: linux-cifs@vger.kernel.org Cc: samba-technical@lists.samba.org Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/accept.c | 2 +- fs/smb/smbdirect/connect.c | 2 +- fs/smb/smbdirect/connection.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/smb/smbdirect/accept.c b/fs/smb/smbdirect/accept.c index e7e5eb75a5ae..85e37d11f43d 100644 --- a/fs/smb/smbdirect/accept.c +++ b/fs/smb/smbdirect/accept.c @@ -234,7 +234,7 @@ static void smbdirect_accept_negotiate_recv_done(struct ib_cq *cq, struct ib_wc struct smbdirect_socket *sc = recv_io->socket; unsigned long flags; - if (unlikely(wc->status != IB_WC_SUCCESS || WARN_ON_ONCE(wc->opcode != IB_WC_RECV))) { + if (unlikely(wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV)) { if (wc->status != IB_WC_WR_FLUSH_ERR) smbdirect_log_rdma_recv(sc, SMBDIRECT_LOG_ERR, "wc->status=%s (%d) wc->opcode=%d\n", diff --git a/fs/smb/smbdirect/connect.c b/fs/smb/smbdirect/connect.c index 34a3e72c38fb..0d02f03063ac 100644 --- a/fs/smb/smbdirect/connect.c +++ b/fs/smb/smbdirect/connect.c @@ -581,7 +581,7 @@ static void smbdirect_connect_negotiate_recv_done(struct ib_cq *cq, struct ib_wc struct smbdirect_socket *sc = recv_io->socket; unsigned long flags; - if (unlikely(wc->status != IB_WC_SUCCESS || WARN_ON_ONCE(wc->opcode != IB_WC_RECV))) { + if (unlikely(wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV)) { if (wc->status != IB_WC_WR_FLUSH_ERR) smbdirect_log_rdma_recv(sc, SMBDIRECT_LOG_ERR, "wc->status=%s (%d) wc->opcode=%d\n", diff --git a/fs/smb/smbdirect/connection.c b/fs/smb/smbdirect/connection.c index f48c857bda5a..e6c7b343fe87 100644 --- a/fs/smb/smbdirect/connection.c +++ b/fs/smb/smbdirect/connection.c @@ -1529,7 +1529,7 @@ void smbdirect_connection_recv_io_done(struct ib_cq *cq, struct ib_wc *wc) u32 data_length; u32 remaining_data_length; - if (unlikely(wc->status != IB_WC_SUCCESS || WARN_ON_ONCE(wc->opcode != IB_WC_RECV))) { + if (unlikely(wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV)) { if (wc->status != IB_WC_WR_FLUSH_ERR) smbdirect_log_rdma_recv(sc, SMBDIRECT_LOG_ERR, "wc->status=%s (%d) wc->opcode=%d\n", From 3474e1a304fbf9f7c996b24adcd519e031227a90 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 17 Oct 2026 09:44:48 +0900 Subject: [PATCH 52/95] ksmbd: validate security descriptor offsets before pointer arithmetic parse_sec_desc() constructs SID and DACL pointers by adding untrusted 32-bit offsets to the security descriptor before checking their bounds. The existing checks reject offsets into the descriptor header, but do not reject offsets beyond the descriptor's length. On a 32-bit kernel, a large offset can wrap the pointer before the descriptor. The end-only checks in parse_sid() and parse_dacl() can then accept it, allowing an authenticated SMB2 SET_INFO request to access memory outside the security descriptor and fault the kernel. Check each nonzero offset against acl_len before constructing its pointer. Initialize the optional owner and group SID pointers to NULL so an omitted SID remains absent when parsing the DACL. Fixes: 0626e6641f6b ("cifsd: add server handler for central processing and tranport layers") Cc: stable@vger.kernel.org Reported-by: Ratnaker Gautam Signed-off-by: Namjae Jeon --- fs/smb/server/smbacl.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index 96428df33b43..f3b1efd8e85a 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -900,7 +900,7 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd, int acl_len, struct smb_fattr *fattr) { int rc = 0; - struct smb_sid *owner_sid_ptr, *group_sid_ptr; + struct smb_sid *owner_sid_ptr = NULL, *group_sid_ptr = NULL; struct smb_acl *dacl_ptr; /* no need for SACL ptr */ char *end_of_acl = ((char *)pntsd) + acl_len; __u32 dacloffset; @@ -912,12 +912,7 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd, if (acl_len < sizeof(struct smb_ntsd)) return -EINVAL; - owner_sid_ptr = (struct smb_sid *)((char *)pntsd + - le32_to_cpu(pntsd->osidoffset)); - group_sid_ptr = (struct smb_sid *)((char *)pntsd + - le32_to_cpu(pntsd->gsidoffset)); dacloffset = le32_to_cpu(pntsd->dacloffset); - dacl_ptr = (struct smb_acl *)((char *)pntsd + dacloffset); ksmbd_debug(SMB, "revision %d type 0x%x ooffset 0x%x goffset 0x%x sacloffset 0x%x dacloffset 0x%x\n", pntsd->revision, pntsd->type, le32_to_cpu(pntsd->osidoffset), @@ -933,9 +928,12 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd, pntsd->type = cpu_to_le16(DACL_PRESENT); if (pntsd->osidoffset) { - if (le32_to_cpu(pntsd->osidoffset) < sizeof(struct smb_ntsd)) + if (le32_to_cpu(pntsd->osidoffset) < sizeof(struct smb_ntsd) || + le32_to_cpu(pntsd->osidoffset) >= acl_len) return -EINVAL; + owner_sid_ptr = (struct smb_sid *)((char *)pntsd + + le32_to_cpu(pntsd->osidoffset)); rc = parse_sid(owner_sid_ptr, end_of_acl); if (rc) { pr_err("%s: Error %d parsing Owner SID\n", __func__, rc); @@ -951,9 +949,12 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd, } if (pntsd->gsidoffset) { - if (le32_to_cpu(pntsd->gsidoffset) < sizeof(struct smb_ntsd)) + if (le32_to_cpu(pntsd->gsidoffset) < sizeof(struct smb_ntsd) || + le32_to_cpu(pntsd->gsidoffset) >= acl_len) return -EINVAL; + group_sid_ptr = (struct smb_sid *)((char *)pntsd + + le32_to_cpu(pntsd->gsidoffset)); rc = parse_sid(group_sid_ptr, end_of_acl); if (rc) { pr_err("%s: Error %d mapping Owner SID to gid\n", @@ -975,9 +976,10 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd, pntsd->type |= cpu_to_le16(DACL_PROTECTED); if (dacloffset) { - if (dacloffset < sizeof(struct smb_ntsd)) + if (dacloffset < sizeof(struct smb_ntsd) || dacloffset >= acl_len) return -EINVAL; + dacl_ptr = (struct smb_acl *)((char *)pntsd + dacloffset); rc = parse_dacl(idmap, dacl_ptr, end_of_acl, owner_sid_ptr, group_sid_ptr, fattr); if (rc) From a5003e47a9e68c0944c1681ba64c81dfbb8b34bc Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 9 Oct 2026 11:37:08 +0900 Subject: [PATCH 53/95] ksmbd: release connection lock while draining old session Previous-session teardown waits for requests on the old channels. Holding srv_mutex during that wait can block their responses and prevent the drain from completing. Record the previous session ID during authentication, finish using the request's authentication state, and release srv_mutex while the old session drains. Keep the response binding decision in the work item because signing and preauthentication hashing run after the drain. Reported-by: Pan Guihua Signed-off-by: Namjae Jeon --- fs/smb/server/ksmbd_work.h | 2 ++ fs/smb/server/mgmt/user_session.c | 15 +++++++++ fs/smb/server/smb2pdu.c | 56 ++++++++++++++++++------------- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 3a14e4d69aa1..40f70a5d08b5 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -94,6 +94,8 @@ struct ksmbd_work { bool need_invalidate_rkey:1; bool request_open_chseq_tracked:1; bool session_setup_reauth:1; + /* Retain the binding decision for response signing and preauth hashing. */ + bool session_setup_binding: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 1321cd6c84e7..d814a99ff519 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -759,6 +759,7 @@ void destroy_previous_session(struct ksmbd_conn *conn, { struct ksmbd_session *prev_sess; struct ksmbd_user *prev_user; + bool pinned = false; int err; down_write(&sessions_table_lock); @@ -781,9 +782,21 @@ void destroy_previous_session(struct ksmbd_conn *conn, } prev_sess->tearing_down = true; up_write(&prev_sess->chann_lock); + ksmbd_user_session_get(prev_sess); + pinned = true; + up_write(&conn->session_lock); + up_write(&sessions_table_lock); ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_NEED_RECONNECT); + /* + * The caller has finished using the connection's authentication state. + * Drop srv_mutex so drained requests can write their responses. + */ + ksmbd_conn_unlock(conn); err = ksmbd_conn_wait_idle_sess(conn, prev_sess); + ksmbd_conn_lock(conn); + down_write(&sessions_table_lock); + down_write(&conn->session_lock); if (err) { down_write(&prev_sess->chann_lock); prev_sess->tearing_down = false; @@ -800,6 +813,8 @@ void destroy_previous_session(struct ksmbd_conn *conn, out: up_write(&conn->session_lock); up_write(&sessions_table_lock); + if (pinned) + ksmbd_user_session_put(prev_sess); } static bool ksmbd_preauth_session_id_match(struct preauth_session *sess, diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index b57942263e13..b3cf974987e6 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2206,14 +2206,14 @@ static struct ksmbd_user *session_user(struct ksmbd_conn *conn, static int ntlm_authenticate(struct ksmbd_work *work, struct smb2_sess_setup_req *req, - struct smb2_sess_setup_rsp *rsp) + struct smb2_sess_setup_rsp *rsp, + u64 *previous_session_id) { struct ksmbd_conn *conn = work->conn; struct ksmbd_session *sess = work->sess; struct ksmbd_user *user; char channel_key[CIFS_KEY_SIZE] = {}; char *auth_key = conn->binding ? channel_key : sess->sess_key; - u64 prev_id; bool binding = conn->binding; int sz, rc; @@ -2278,9 +2278,7 @@ static int ntlm_authenticate(struct ksmbd_work *work, } } - prev_id = le64_to_cpu(req->PreviousSessionId); - if (prev_id && prev_id != sess->id) - destroy_previous_session(conn, sess->user, prev_id); + *previous_session_id = le64_to_cpu(req->PreviousSessionId); /* * If session state is SMB2_SESSION_VALID, We can assume @@ -2342,7 +2340,8 @@ static int ntlm_authenticate(struct ksmbd_work *work, #ifdef CONFIG_SMB_SERVER_KERBEROS5 static int krb5_authenticate(struct ksmbd_work *work, struct smb2_sess_setup_req *req, - struct smb2_sess_setup_rsp *rsp) + struct smb2_sess_setup_rsp *rsp, + u64 *previous_session_id) { struct ksmbd_conn *conn = work->conn; struct ksmbd_session *sess = work->sess; @@ -2351,7 +2350,6 @@ static int krb5_authenticate(struct ksmbd_work *work, 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; int retval; @@ -2373,10 +2371,7 @@ static int krb5_authenticate(struct ksmbd_work *work, goto out; } - /* Check previous session */ - prev_sess_id = le64_to_cpu(req->PreviousSessionId); - if (prev_sess_id && prev_sess_id != sess->id) - destroy_previous_session(conn, sess->user, prev_sess_id); + *previous_session_id = le64_to_cpu(req->PreviousSessionId); rsp->SecurityBufferLength = cpu_to_le16(out_len); @@ -2445,7 +2440,8 @@ static int krb5_authenticate(struct ksmbd_work *work, #else static int krb5_authenticate(struct ksmbd_work *work, struct smb2_sess_setup_req *req, - struct smb2_sess_setup_rsp *rsp) + struct smb2_sess_setup_rsp *rsp, + u64 *previous_session_id) { return -EOPNOTSUPP; } @@ -2459,6 +2455,8 @@ int smb2_sess_setup(struct ksmbd_work *work) struct ksmbd_session *sess = NULL; struct negotiate_message *negblob; unsigned int negblob_len, negblob_off; + u64 previous_session_id = 0; + bool authenticated = false; int rc = 0; ksmbd_debug(SMB, "Received smb2 session setup request\n"); @@ -2648,14 +2646,11 @@ 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); + rc = krb5_authenticate(work, req, rsp, &previous_session_id); if (rc) goto out_err; - if (!ksmbd_conn_need_reconnect(conn)) { - ksmbd_conn_set_good(conn); - sess->state = SMB2_SESSION_VALID; - } + authenticated = true; } else if (conn->preferred_auth_mech == KSMBD_AUTH_NTLMSSP) { if (negblob->MessageType == NtLmNegotiate) { rc = ntlm_negotiate(work, negblob, negblob_len, rsp); @@ -2664,14 +2659,12 @@ int smb2_sess_setup(struct ksmbd_work *work) rsp->hdr.Status = STATUS_MORE_PROCESSING_REQUIRED; } else if (negblob->MessageType == NtLmAuthenticate) { - rc = ntlm_authenticate(work, req, rsp); + rc = ntlm_authenticate(work, req, rsp, + &previous_session_id); if (rc) goto out_err; - if (!ksmbd_conn_need_reconnect(conn)) { - ksmbd_conn_set_good(conn); - sess->state = SMB2_SESSION_VALID; - } + authenticated = true; if (conn->binding) { struct preauth_session *preauth_sess; @@ -2804,6 +2797,21 @@ int smb2_sess_setup(struct ksmbd_work *work) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; } + work->session_setup_binding = conn->binding; + if (authenticated) { + /* + * Draining the previous session drops srv_mutex. Finish using the + * connection's authentication state and free mechToken first, and + * retain this request's binding decision for response processing. + */ + if (previous_session_id && previous_session_id != sess->id) + destroy_previous_session(conn, sess->user, previous_session_id); + if (!ksmbd_conn_need_reconnect(conn)) { + ksmbd_conn_set_good(conn); + sess->state = SMB2_SESSION_VALID; + } + } + ksmbd_conn_unlock(conn); return rc; } @@ -12478,7 +12486,7 @@ void smb3_set_sign_rsp(struct ksmbd_work *work) command = le16_to_cpu(hdr->Command); if (command == SMB2_SESSION_SETUP_HE && - (!conn->binding || hdr->Status != STATUS_SUCCESS)) { + (!work->session_setup_binding || hdr->Status != STATUS_SUCCESS)) { signing_key = work->sess->smb3signingkey; } else { chann = lookup_chann_list(work->sess, work->conn); @@ -12527,7 +12535,7 @@ void smb3_preauth_hash_rsp(struct ksmbd_work *work) if (le16_to_cpu(rsp->Command) == SMB2_SESSION_SETUP_HE && sess) { ksmbd_conn_lock(conn); - if (conn->binding) { + if (work->session_setup_binding) { struct preauth_session *preauth_sess; preauth_sess = ksmbd_preauth_session_lookup(conn, sess->id); From 6c483449b84990cb06f5071cf2067f794e558187 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 9 Oct 2026 11:37:24 +0900 Subject: [PATCH 54/95] ksmbd: serialize oplock break ACK state updates ACK handling updates lease or oplock levels and wakes break waiters. Without state_lock across validation and publication, timeout or break completion can observe a partially updated state. Serialize ACK validation and state changes under state_lock. Publish NONE after lease levels are updated, then wake waiters after unlocking. Reported-by: Pan Guihua Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 72 +++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index b3cf974987e6..4278b0f853ae 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -11919,6 +11919,8 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) return; } + /* Serialize the level update with break completion and timeout. */ + spin_lock(&opinfo->state_lock); if (opinfo->op_state != OPLOCK_ACK_WAIT) { ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", opinfo->op_state); @@ -11930,6 +11932,7 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) rsp->Reserved2 = 0; rsp->VolatileFid = volatile_id; rsp->PersistentFid = persistent_id; + spin_unlock(&opinfo->state_lock); ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_oplock_break)); if (ret) @@ -11986,6 +11989,9 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) rsp_oplevel = req_oplevel; opinfo->level = rsp_oplevel; + opinfo->op_state = OPLOCK_STATE_NONE; + spin_unlock(&opinfo->state_lock); + wake_up_interruptible_all(&opinfo->oplock_q); rsp->StructureSize = cpu_to_le16(24); rsp->OplockLevel = rsp_oplevel; @@ -11997,18 +12003,15 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) if (ret) ksmbd_debug(SMB, "failed to pin oplock break response: %d\n", ret); - goto out; + goto out_no_state_change; err_out: - rsp->hdr.Status = status; - smb2_set_err_rsp(work); - -out: - 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); + rsp->hdr.Status = status; + smb2_set_err_rsp(work); out_no_state_change: opinfo_put(opinfo); ksmbd_fd_put(work, fp); @@ -12042,7 +12045,7 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) struct smb2_lease_ack *req; struct smb2_lease_ack *rsp; struct oplock_info *opinfo; - int ret = 0; + int ret; __le32 lease_state; struct lease *lease; @@ -12053,26 +12056,27 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) opinfo = lookup_lease_in_table(conn, req->LeaseKey); if (!opinfo) { ksmbd_debug(OPLOCK, "file not opened\n"); - smb2_set_err_rsp(work); rsp->hdr.Status = STATUS_UNSUCCESSFUL; + smb2_set_err_rsp(work); return; } lease = opinfo->o_lease; - 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)) + /* Publish NONE only after the lease state and all open levels are updated. */ + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_ACK_WAIT) { + if (opinfo->op_state == OPLOCK_STATE_NONE && + smb3_hdr_replay(&req->hdr)) goto replay_rsp; rsp->hdr.Status = STATUS_UNSUCCESSFUL; - goto err_out; + goto err_unlock; } if (!atomic_read(&opinfo->breaking_cnt)) { if (smb3_hdr_replay(&req->hdr)) goto replay_rsp; rsp->hdr.Status = STATUS_UNSUCCESSFUL; - goto err_out; + goto err_unlock; } if (check_lease_state(lease, req->LeaseState)) { @@ -12080,51 +12084,41 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) ksmbd_debug(OPLOCK, "req lease state: 0x%x, expected state: 0x%x\n", req->LeaseState, lease->new_state); - goto err_out; + goto err_unlock; } lease_state = req->LeaseState; lease->state = lease_state; lease->new_state = SMB2_LEASE_NONE_LE; lease_update_oplock_levels(lease); + opinfo->op_state = OPLOCK_STATE_NONE; + atomic_dec_if_positive(&opinfo->breaking_cnt); + spin_unlock(&opinfo->state_lock); + wake_up_interruptible_all(&opinfo->oplock_q); + wake_up_interruptible_all(&opinfo->oplock_brk); + goto response; +replay_rsp: + lease_state = lease->state; + spin_unlock(&opinfo->state_lock); +response: rsp->StructureSize = cpu_to_le16(36); rsp->Reserved = 0; rsp->Flags = 0; - memcpy(rsp->LeaseKey, req->LeaseKey, 16); + memcpy(rsp->LeaseKey, req->LeaseKey, SMB2_LEASE_KEY_SIZE); 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; - spin_lock(&opinfo->state_lock); - if (opinfo->op_state != OPLOCK_CLOSING) - opinfo->op_state = OPLOCK_STATE_NONE; +err_unlock: spin_unlock(&opinfo->state_lock); - wake_up_interruptible_all(&opinfo->oplock_q); - atomic_dec_if_positive(&opinfo->breaking_cnt); - wake_up_interruptible_all(&opinfo->oplock_brk); - 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); - return; } /** From c0632d2010ffc006eed9becc8d0d10d54f294bbd Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 9 Oct 2026 11:37:33 +0900 Subject: [PATCH 55/95] ksmbd: track delivery of queued TCP writes A nonblocking break send must distinguish a PDU accepted by TCP from one still queued in ksmbd. Retrying an accepted PDU can send duplicates, while abandoning a partial send can corrupt the stream. Queue owned PDU bytes when TCP cannot accept the full message. Track the stream position through socket delivery and TCP ACK, and expose it to break retries. Bound retained data and stop queued writes during teardown. Reported-by: Pan Guihua Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 52 +++++- fs/smb/server/connection.h | 24 ++- fs/smb/server/transport_tcp.c | 299 +++++++++++++++++++++++++++++++++- 3 files changed, 356 insertions(+), 19 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index d1cc725eeb06..19d32681bb92 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -457,17 +457,13 @@ void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status) 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)) { + 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) + if (conn->transport->ops->shutdown) conn->transport->ops->shutdown(conn->transport); } @@ -534,12 +530,19 @@ static int __ksmbd_conn_write(struct ksmbd_work *work, tx->need_invalidate_rkey = work->need_invalidate_rkey; tx->remote_key = work->remote_key; - ksmbd_conn_lock(conn); + if (tx->tracking) { + /* A busy writer must not consume the break's retry budget. */ + if (!mutex_trylock(&conn->srv_mutex)) + return -EAGAIN; + } else { + ksmbd_conn_lock(conn); + } sent = conn->transport->ops->writev(conn->transport, tx); ksmbd_conn_unlock(conn); if (sent < 0) { - pr_err("Failed to send message: %d\n", sent); + if (!tx->tracking || sent != -EAGAIN) + pr_err("Failed to send message: %d\n", sent); return sent; } @@ -553,6 +556,20 @@ int ksmbd_conn_write(struct ksmbd_work *work) return __ksmbd_conn_write(work, &tx); } +int ksmbd_conn_write_tracked(struct ksmbd_work *work, + struct ksmbd_tx_tracking *tracking) +{ + struct ksmbd_transport_write tx = {}; + + tracking->valid = false; + /* Transports without delivery tracking keep their synchronous send path. */ + if (work->conn->transport->ops->is_acked) { + tx.msg_flags = MSG_DONTWAIT; + tx.tracking = tracking; + } + return __ksmbd_conn_write(work, &tx); +} + int ksmbd_conn_write_eor(struct ksmbd_work *work) { struct ksmbd_transport_write tx = { @@ -805,7 +822,12 @@ int ksmbd_conn_handler_loop(void *p) } } + /* Exclude new break senders before draining their work references. */ + spin_lock(&conn->request_lock); ksmbd_conn_set_releasing(conn); + spin_unlock(&conn->request_lock); + /* TCP writers waiting for queue space must release their work references. */ + wake_up_all(&conn->req_running_q); 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)); @@ -832,6 +854,20 @@ void ksmbd_conn_r_count_inc(struct ksmbd_conn *conn) atomic_inc(&conn->r_count); } +/* The caller already owns a connection reference. */ +bool ksmbd_conn_r_count_inc_if_good(struct ksmbd_conn *conn) +{ + bool ret = false; + + spin_lock(&conn->request_lock); + if (ksmbd_conn_good(conn)) { + atomic_inc(&conn->r_count); + ret = true; + } + spin_unlock(&conn->request_lock); + return ret; +} + void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn) { /* diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 2026a2afa5e4..85e2d2073cd2 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -103,6 +103,8 @@ struct ksmbd_conn { wait_queue_head_t r_count_q; /* Lock to protect requests list*/ spinlock_t request_lock; + /* Set under request_lock at teardown start and kept set thereafter. */ + bool releasing; struct list_head requests; struct list_head async_requests; int connection_type; @@ -162,6 +164,13 @@ struct ksmbd_conn_ops { int (*terminate_fn)(struct ksmbd_conn *conn); }; +struct ksmbd_tx_tracking { + /* Required TCP byte count, including transport and socket queues. */ + u64 end; + unsigned long retry_after; + bool valid; +}; + struct ksmbd_transport_write { struct kvec *iov; int iov_cnt; @@ -169,6 +178,7 @@ struct ksmbd_transport_write { bool need_invalidate_rkey; unsigned int remote_key; int msg_flags; + struct ksmbd_tx_tracking *tracking; }; struct ksmbd_transport_ops { @@ -176,8 +186,12 @@ struct ksmbd_transport_ops { void (*shutdown)(struct ksmbd_transport *t); int (*read)(struct ksmbd_transport *t, char *buf, unsigned int size, int max_retries); + /* Retained bytes must be privately owned before a full result is returned. */ int (*writev)(struct ksmbd_transport *t, const struct ksmbd_transport_write *tx); + /* Optional delivery tracking for nonblocking break sends. */ + bool (*is_acked)(struct ksmbd_transport *t, + const struct ksmbd_tx_tracking *tracking); int (*rdma_read)(struct ksmbd_transport *t, void *buf, unsigned int len, struct smbdirect_buffer_descriptor_v1 *desc, @@ -217,6 +231,8 @@ 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_tracked(struct ksmbd_work *work, + struct ksmbd_tx_tracking *tracking); int ksmbd_conn_write_eor(struct ksmbd_work *work); int ksmbd_conn_rdma_read(struct ksmbd_conn *conn, void *buf, unsigned int buflen, @@ -235,6 +251,7 @@ void ksmbd_conn_transport_destroy(void); void ksmbd_conn_lock(struct ksmbd_conn *conn); void ksmbd_conn_unlock(struct ksmbd_conn *conn); void ksmbd_conn_r_count_inc(struct ksmbd_conn *conn); +bool ksmbd_conn_r_count_inc_if_good(struct ksmbd_conn *conn); void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn); /* @@ -250,7 +267,8 @@ static inline bool ksmbd_conn_new(struct ksmbd_conn *conn) static inline bool ksmbd_conn_good(struct ksmbd_conn *conn) { - return READ_ONCE(conn->status) == KSMBD_SESS_GOOD; + return !READ_ONCE(conn->releasing) && + READ_ONCE(conn->status) == KSMBD_SESS_GOOD; } static inline unsigned int @@ -284,7 +302,8 @@ static inline bool ksmbd_conn_exiting(struct ksmbd_conn *conn) static inline bool ksmbd_conn_releasing(struct ksmbd_conn *conn) { - return READ_ONCE(conn->status) == KSMBD_SESS_RELEASING; + return READ_ONCE(conn->releasing) || + READ_ONCE(conn->status) == KSMBD_SESS_RELEASING; } static inline void ksmbd_conn_set_new(struct ksmbd_conn *conn) @@ -319,6 +338,7 @@ static inline void ksmbd_conn_set_exiting(struct ksmbd_conn *conn) static inline void ksmbd_conn_set_releasing(struct ksmbd_conn *conn) { + WRITE_ONCE(conn->releasing, true); WRITE_ONCE(conn->status, KSMBD_SESS_RELEASING); } diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 488ca113b431..9c6ca878d30f 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -5,6 +5,7 @@ */ #include +#include #include "smb_common.h" #include "server.h" @@ -29,11 +30,30 @@ static LIST_HEAD(iface_list); static int bind_additional_ifaces; +struct tcp_tx { + struct list_head list; + size_t len; + size_t offset; + unsigned long deadline; + int msg_flags; + u8 data[]; +}; + struct tcp_transport { struct ksmbd_transport transport; struct socket *sock; struct kvec *iov; unsigned int nr_iov; + /* Serializes queue access and socket writes. Never taken by callbacks. */ + struct mutex tx_lock; + struct list_head tx_queue; + struct delayed_work tx_work; + size_t tx_queued; + /* TCP byte positions for accepted PDUs and bytes passed to the socket. */ + u64 tx_end; + u64 tx_sent; + bool tx_stopped; + void (*write_space)(struct sock *sk); }; static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops; @@ -42,6 +62,10 @@ static void tcp_stop_kthread(struct task_struct *kthread); static void ksmbd_tcp_stop_listener(struct interface *iface); static struct interface *alloc_iface(char *ifname); static void ksmbd_tcp_disconnect(struct ksmbd_transport *t); +static void ksmbd_tcp_shutdown(struct ksmbd_transport *t); +static void ksmbd_tcp_tx_work(struct work_struct *work); +static void ksmbd_tcp_write_space(struct sock *sk); +static void ksmbd_tcp_stop_tx(struct tcp_transport *t); #define KSMBD_TRANS(t) (&(t)->transport) #define TCP_TRANS(t) ((struct tcp_transport *)container_of(t, \ @@ -66,6 +90,9 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk) if (!t) return NULL; t->sock = client_sk; + mutex_init(&t->tx_lock); + INIT_LIST_HEAD(&t->tx_queue); + INIT_DELAYED_WORK(&t->tx_work, ksmbd_tcp_tx_work); conn = ksmbd_conn_alloc(); if (!conn) { @@ -86,13 +113,26 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk) conn->inet_addr = inet_sk(client_sk->sk)->inet_daddr; conn->inet_hash = ipv4_addr_hash(inet_sk(client_sk->sk)->inet_daddr); #endif - down_write(&conn_list_lock); - hash_add(conn_list, &conn->hlist, conn->inet_hash); - up_write(&conn_list_lock); - conn->transport = KSMBD_TRANS(t); KSMBD_TRANS(t)->conn = conn; KSMBD_TRANS(t)->ops = &ksmbd_tcp_transport_ops; + + lock_sock(client_sk->sk); + t->tx_end = tcp_sk(client_sk->sk)->bytes_acked + + (u32)(tcp_sk(client_sk->sk)->write_seq - + tcp_sk(client_sk->sk)->snd_una); + t->tx_sent = t->tx_end; + release_sock(client_sk->sk); + + write_lock_bh(&client_sk->sk->sk_callback_lock); + t->write_space = client_sk->sk->sk_write_space; + client_sk->sk->sk_user_data = t; + client_sk->sk->sk_write_space = ksmbd_tcp_write_space; + write_unlock_bh(&client_sk->sk->sk_callback_lock); + + down_write(&conn_list_lock); + hash_add(conn_list, &conn->hlist, conn->inet_hash); + up_write(&conn_list_lock); return t; } @@ -100,6 +140,7 @@ static void ksmbd_tcp_free_transport(struct ksmbd_transport *kt) { struct tcp_transport *t = TCP_TRANS(kt); + ksmbd_tcp_stop_tx(t); sock_release(t->sock); kfree(t->iov); kfree(t); @@ -107,7 +148,8 @@ static void ksmbd_tcp_free_transport(struct ksmbd_transport *kt) static void free_transport(struct tcp_transport *t) { - kernel_sock_shutdown(t->sock, SHUT_RDWR); + ksmbd_tcp_shutdown(KSMBD_TRANS(t)); + ksmbd_tcp_stop_tx(t); ksmbd_conn_free(KSMBD_TRANS(t)->conn); } @@ -432,15 +474,251 @@ 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 bool ksmbd_tcp_tx_has_space(struct tcp_transport *t, size_t size) +{ + size_t limit = max_t(size_t, READ_ONCE(t->sock->sk->sk_sndbuf), size); + + /* Allow one large response, but bound the retained response buffers. */ + return READ_ONCE(t->tx_queued) <= limit - size; +} + +static struct tcp_tx *ksmbd_tcp_alloc_tx(const struct ksmbd_transport_write *tx, + size_t offset) +{ + struct tcp_tx *msg; + struct iov_iter iter; + size_t remaining = tx->size - offset; + + msg = kvmalloc(struct_size(msg, data, remaining), KSMBD_DEFAULT_GFP); + if (!msg) + return NULL; + + iov_iter_kvec(&iter, ITER_SOURCE, tx->iov, tx->iov_cnt, tx->size); + iov_iter_advance(&iter, offset); + if (!copy_from_iter_full(msg->data, remaining, &iter)) { + kvfree(msg); + return NULL; + } + msg->len = remaining; + msg->offset = 0; + msg->msg_flags = tx->msg_flags; + return msg; +} + +static void ksmbd_tcp_purge_tx(struct tcp_transport *t) +{ + struct tcp_tx *msg, *next; + + list_for_each_entry_safe(msg, next, &t->tx_queue, list) { + list_del(&msg->list); + kvfree(msg); + } + WRITE_ONCE(t->tx_queued, 0); +} + +static bool ksmbd_tcp_send_retry(int ret) +{ + return ret == -EAGAIN || ret == -EINTR || ret == -ERESTARTSYS || + ret == -ENOMEM; +} + +static void ksmbd_tcp_tx_work(struct work_struct *work) +{ + struct tcp_transport *t = container_of(to_delayed_work(work), + struct tcp_transport, tx_work); + unsigned long delay = 0; + unsigned int budget = 16; + bool failed = false; + int ret; + + mutex_lock(&t->tx_lock); + while (!READ_ONCE(t->tx_stopped) && !list_empty(&t->tx_queue) && budget--) { + struct tcp_tx *msg = list_first_entry(&t->tx_queue, struct tcp_tx, list); + struct msghdr hdr = { + .msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT | msg->msg_flags, + }; + struct kvec iov = { + .iov_base = msg->data + msg->offset, + .iov_len = msg->len - msg->offset, + }; + + /* MSG_DONTWAIT bypasses sk_sndtimeo, including for retained PDUs. */ + if (time_after_eq(jiffies, msg->deadline)) { + WRITE_ONCE(t->tx_stopped, true); + failed = true; + break; + } + ret = kernel_sendmsg(t->sock, &hdr, &iov, 1, iov.iov_len); + if (ret > 0) { + msg->offset += ret; + t->tx_sent += ret; + if (msg->offset == msg->len) { + WRITE_ONCE(t->tx_queued, t->tx_queued - msg->len); + list_del(&msg->list); + kvfree(msg); + continue; + } + } else if (!ksmbd_tcp_send_retry(ret)) { + WRITE_ONCE(t->tx_stopped, true); + failed = true; + break; + } + /* + * Write-space notifications normally resume the head of the queue. + * Also retry after memory pressure, which may produce no notification. + */ + delay = max_t(unsigned long, 1, HZ / 10); + break; + } + if (READ_ONCE(t->tx_stopped)) + ksmbd_tcp_purge_tx(t); + else if (!list_empty(&t->tx_queue)) + queue_delayed_work(system_unbound_wq, &t->tx_work, delay); + mutex_unlock(&t->tx_lock); + wake_up_all(&t->transport.conn->req_running_q); + + if (failed) + ksmbd_conn_abort(t->transport.conn); +} + +static void ksmbd_tcp_write_space(struct sock *sk) +{ + struct tcp_transport *t; + + read_lock_bh(&sk->sk_callback_lock); + t = sk->sk_user_data; + if (t) { + t->write_space(sk); + if (!READ_ONCE(t->tx_stopped) && sk_stream_is_writeable(sk)) + mod_delayed_work(system_unbound_wq, &t->tx_work, 0); + } + read_unlock_bh(&sk->sk_callback_lock); +} + +static void ksmbd_tcp_stop_tx(struct tcp_transport *t) +{ + struct sock *sk = t->sock->sk; + + WRITE_ONCE(t->tx_stopped, true); + write_lock_bh(&sk->sk_callback_lock); + if (sk->sk_user_data == t) { + sk->sk_write_space = t->write_space; + sk->sk_user_data = NULL; + } + write_unlock_bh(&sk->sk_callback_lock); + cancel_delayed_work_sync(&t->tx_work); + mutex_lock(&t->tx_lock); + ksmbd_tcp_purge_tx(t); + mutex_unlock(&t->tx_lock); + wake_up_all(&t->transport.conn->req_running_q); +} + static int ksmbd_tcp_writev(struct ksmbd_transport *t, const struct ksmbd_transport_write *tx) { - struct msghdr smb_msg = { - .msg_flags = MSG_NOSIGNAL | tx->msg_flags, + struct tcp_transport *tt = TCP_TRANS(t); + struct sock *sk = tt->sock->sk; + struct msghdr hdr = { + .msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT | tx->msg_flags, }; + struct tcp_tx *msg; + unsigned long deadline = jiffies + KSMBD_TCP_SEND_TIMEOUT; + long timeout; + int sent = 0; - return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov, - tx->iov_cnt, tx->size); + for (;;) { + if (tx->msg_flags & MSG_DONTWAIT) { + if (!mutex_trylock(&tt->tx_lock)) + return -EAGAIN; + } else { + mutex_lock(&tt->tx_lock); + } + if (READ_ONCE(tt->tx_stopped) || ksmbd_conn_exiting(t->conn) || + ksmbd_conn_releasing(t->conn)) { + mutex_unlock(&tt->tx_lock); + return -EPIPE; + } + if (ksmbd_tcp_tx_has_space(tt, tx->size)) + break; + mutex_unlock(&tt->tx_lock); + if (tx->msg_flags & MSG_DONTWAIT) + return -EAGAIN; + timeout = deadline - jiffies; + if (time_after_eq(jiffies, deadline) || + !wait_event_timeout(t->conn->req_running_q, + READ_ONCE(tt->tx_stopped) || + ksmbd_conn_exiting(t->conn) || + ksmbd_conn_releasing(t->conn) || + ksmbd_tcp_tx_has_space(tt, tx->size), + timeout)) { + /* A queued PDU may already have a prefix in the socket. */ + ksmbd_conn_abort(t->conn); + return -EAGAIN; + } + } + + /* Avoid an extra copy when the socket accepts the whole PDU at once. */ + if (list_empty(&tt->tx_queue)) { + sent = kernel_sendmsg(tt->sock, &hdr, tx->iov, tx->iov_cnt, tx->size); + if (sent > 0) { + tt->tx_sent += sent; + if (sent == tx->size) + goto accepted; + } else if (ksmbd_tcp_send_retry(sent)) { + sent = 0; + } else { + WRITE_ONCE(tt->tx_stopped, true); + mutex_unlock(&tt->tx_lock); + ksmbd_conn_abort(t->conn); + return sent ?: -EPIPE; + } + } + + /* The request buffers can be freed as soon as writev returns. */ + msg = ksmbd_tcp_alloc_tx(tx, sent); + if (!msg) { + if (sent) + WRITE_ONCE(tt->tx_stopped, true); + mutex_unlock(&tt->tx_lock); + if (sent) + ksmbd_conn_abort(t->conn); + return -ENOMEM; + } + msg->deadline = deadline; + list_add_tail(&msg->list, &tt->tx_queue); + WRITE_ONCE(tt->tx_queued, tt->tx_queued + msg->len); + queue_delayed_work(system_unbound_wq, &tt->tx_work, 0); + +accepted: + tt->tx_end += tx->size; + if (tx->tracking) { + /* Include retained data, without retaining the caller's tracking object. */ + tx->tracking->end = tt->tx_end; + lock_sock(sk); + tx->tracking->retry_after = + clamp_t(unsigned long, 6UL * inet_csk(sk)->icsk_rto, + msecs_to_jiffies(1200), 6 * HZ); + release_sock(sk); + tx->tracking->valid = true; + } + mutex_unlock(&tt->tx_lock); + return tx->size; +} + +static bool ksmbd_tcp_is_acked(struct ksmbd_transport *t, + const struct ksmbd_tx_tracking *tracking) +{ + struct tcp_transport *tt = TCP_TRANS(t); + struct sock *sk = tt->sock->sk; + bool acked; + + mutex_lock(&tt->tx_lock); + lock_sock(sk); + acked = tt->tx_sent >= tracking->end && + tcp_sk(sk)->bytes_acked >= tracking->end; + release_sock(sk); + mutex_unlock(&tt->tx_lock); + return acked; } static void ksmbd_tcp_disconnect(struct ksmbd_transport *t) @@ -452,7 +730,9 @@ static void ksmbd_tcp_disconnect(struct ksmbd_transport *t) static void ksmbd_tcp_shutdown(struct ksmbd_transport *t) { + WRITE_ONCE(TCP_TRANS(t)->tx_stopped, true); kernel_sock_shutdown(TCP_TRANS(t)->sock, SHUT_RDWR); + wake_up_all(&t->conn->req_running_q); } static void tcp_destroy_socket(struct socket *ksmbd_socket) @@ -721,6 +1001,7 @@ int ksmbd_tcp_set_interfaces(char *ifc_list, int ifc_list_sz) static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops = { .read = ksmbd_tcp_read, .writev = ksmbd_tcp_writev, + .is_acked = ksmbd_tcp_is_acked, .disconnect = ksmbd_tcp_disconnect, .shutdown = ksmbd_tcp_shutdown, .free_transport = ksmbd_tcp_free_transport, From 14ad1f21fbf05179116dd6ef4ab3b6556478fa50 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 9 Oct 2026 11:38:15 +0900 Subject: [PATCH 56/95] ksmbd: retry break notifications on another connection An oplock or lease break belongs to an open that can outlive its original channel. A draining channel or stalled TCP writer can otherwise leave the break pending until timeout or invalidate a durable open. Snapshot the owner's live channels and try eligible connections with the same notification. Track TCP delivery separately from the SMB ACK, retry when a channel stops progressing, and share one break deadline. Run ordinary break waits on a dedicated workqueue so blocked request responses do not stall breaks. Publish NONE before sending a level II break, which requires no ACK, so an unsolicited ACK observes the completed level while TCP delivery is pending. Keep the first rename lease notification in the request context and enqueue the interim writer only after the notification is accepted. Join that work before returning so a blocked interim response does not stop notification retries or release the request early. Reported-by: Pan Guihua Signed-off-by: Namjae Jeon --- fs/smb/server/ksmbd_work.c | 19 + fs/smb/server/ksmbd_work.h | 2 + fs/smb/server/mgmt/user_session.c | 31 ++ fs/smb/server/mgmt/user_session.h | 3 + fs/smb/server/oplock.c | 865 +++++++++++++++++------------- fs/smb/server/oplock.h | 14 +- fs/smb/server/vfs_cache.c | 2 + 7 files changed, 544 insertions(+), 392 deletions(-) diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index 52ea1a674a67..6e5122e2d6fc 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -16,6 +16,7 @@ static struct kmem_cache *work_cache; static struct workqueue_struct *ksmbd_wq; +static struct workqueue_struct *ksmbd_break_wq; static int ksmbd_reserve_iov(struct ksmbd_work *work, int need_iov_cnt) { @@ -110,6 +111,12 @@ int ksmbd_workqueue_init(void) ksmbd_wq = alloc_workqueue("ksmbd-io", WQ_PERCPU, 0); if (!ksmbd_wq) return -ENOMEM; + ksmbd_break_wq = alloc_workqueue("ksmbd-break", WQ_UNBOUND, 0); + if (!ksmbd_break_wq) { + destroy_workqueue(ksmbd_wq); + ksmbd_wq = NULL; + return -ENOMEM; + } return 0; } @@ -117,6 +124,8 @@ void ksmbd_workqueue_destroy(void) { destroy_workqueue(ksmbd_wq); ksmbd_wq = NULL; + destroy_workqueue(ksmbd_break_wq); + ksmbd_break_wq = NULL; } bool ksmbd_queue_work(struct ksmbd_work *work) @@ -124,6 +133,16 @@ bool ksmbd_queue_work(struct ksmbd_work *work) return queue_work(ksmbd_wq, &work->work); } +void ksmbd_queue_io_work(struct work_struct *work) +{ + queue_work(ksmbd_wq, work); +} + +void ksmbd_queue_break_work(struct work_struct *work) +{ + queue_work(ksmbd_break_wq, work); +} + static inline void __ksmbd_iov_pin(struct ksmbd_work *work, void *ib, unsigned int ib_len) { diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 40f70a5d08b5..b51dea69cb0e 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -155,6 +155,8 @@ int ksmbd_work_pool_init(void); int ksmbd_workqueue_init(void); void ksmbd_workqueue_destroy(void); bool ksmbd_queue_work(struct ksmbd_work *work); +void ksmbd_queue_io_work(struct work_struct *work); +void ksmbd_queue_break_work(struct work_struct *work); int ksmbd_iov_pin_rsp_read(struct ksmbd_work *work, void *ib, int len, void *aux_buf, unsigned int aux_size); int ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len); diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index d814a99ff519..554434c7673a 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -674,6 +674,37 @@ struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id) return sess; } +/* The caller holds a session reference and must put each returned connection. */ +unsigned int ksmbd_session_get_channels(struct ksmbd_session *sess, + struct ksmbd_conn **conns, + unsigned int max_conns) +{ + struct channel *chann; + unsigned long index; + unsigned int nr_conns = 0; + + /* + * Channel connections are borrowed. Unregister can leave the channel + * list in a pinned session after removing it from the registry, so + * chann_lock alone does not keep those connections alive. Only collect + * registered sessions, excluding both unregister and connection cleanup + * until the connection references have been taken. + */ + down_read(&sessions_table_lock); + if (hlist_unhashed(&sess->hlist)) + goto out; + down_read(&sess->chann_lock); + xa_for_each(&sess->ksmbd_chann_list, index, chann) { + if (nr_conns == max_conns) + break; + conns[nr_conns++] = ksmbd_conn_get(chann->conn); + } + up_read(&sess->chann_lock); +out: + up_read(&sessions_table_lock); + return nr_conns; +} + struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn, unsigned long long id) { diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h index 217258551d6d..aff31f1694db 100644 --- a/fs/smb/server/mgmt/user_session.h +++ b/fs/smb/server/mgmt/user_session.h @@ -94,6 +94,9 @@ struct ksmbd_session *ksmbd_smb2_session_create(void); void ksmbd_session_destroy(struct ksmbd_session *sess); struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id); +unsigned int ksmbd_session_get_channels(struct ksmbd_session *sess, + struct ksmbd_conn **conns, + unsigned int max_conns); struct ksmbd_session *ksmbd_session_lookup(struct ksmbd_conn *conn, unsigned long long id); bool is_ksmbd_session_in_connection(struct ksmbd_conn *conn, diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 4830d30d07a7..eedbbf9b05d0 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -87,6 +87,7 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work, return NULL; opinfo->sess = sess; + opinfo->session_id = sess->id; opinfo->conn = ksmbd_conn_get(work->conn); opinfo->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->op_state = OPLOCK_STATE_NONE; @@ -302,15 +303,9 @@ static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci, opinfo = list_first_entry_or_null(&ci->m_op_list, struct oplock_info, op_entry); if (opinfo) { - if (opinfo->conn == NULL || - !atomic_inc_not_zero(&opinfo->refcount)) + /* A live open may still be bound to a disconnected channel. */ + if (!atomic_inc_not_zero(&opinfo->refcount)) opinfo = NULL; - else { - if (ksmbd_conn_releasing(opinfo->conn)) { - atomic_dec(&opinfo->refcount); - opinfo = NULL; - } - } if (opinfo && snapshot && opinfo->o_fp && opinfo->o_fp != skip_fp && @@ -367,7 +362,7 @@ static void opinfo_del(struct oplock_info *opinfo) lease_del_open(opinfo); down_write(&ci->m_lock); - list_del(&opinfo->op_entry); + list_del_init(&opinfo->op_entry); up_write(&ci->m_lock); } @@ -639,19 +634,27 @@ static void grant_none_oplock(struct oplock_info *opinfo_new, static inline int compare_guid_key(struct oplock_info *opinfo, const char *guid1, const char *key1) { - const char *guid2, *key2; + struct lease *lease = opinfo->o_lease; + struct lease_table *lb; struct ksmbd_conn *conn; + int ret = 0; - conn = READ_ONCE(opinfo->conn); - if (!conn) + if (memcmp(key1, lease->lease_key, SMB2_LEASE_KEY_SIZE)) return 0; - guid2 = conn->ClientGUID; - key2 = opinfo->o_lease->lease_key; - if (!memcmp(guid1, guid2, SMB2_CLIENT_GUID_SIZE) && - !memcmp(key1, key2, SMB2_LEASE_KEY_SIZE)) - return 1; - return 0; + /* The caller holds ci->m_lock, which protects opinfo->conn. */ + conn = READ_ONCE(opinfo->conn); + if (conn) + return !memcmp(guid1, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); + + /* A detached durable lease retains its client identity in the table. */ + read_lock(&lease_list_lock); + lb = lease->l_lb; + if (lb) + ret = !memcmp(guid1, lb->client_guid, SMB2_CLIENT_GUID_SIZE); + read_unlock(&lease_list_lock); + + return ret; } /** @@ -681,7 +684,7 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, */ down_read(&ci->m_lock); list_for_each_entry(opinfo, &ci->m_op_list, op_entry) { - if (!opinfo->is_lease || !opinfo->conn) + if (!opinfo->is_lease) continue; lease = opinfo->o_lease; @@ -734,35 +737,6 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, return m_opinfo; } -static bool wait_for_break_ack(struct oplock_info *opinfo) -{ - int rc = 0; - - rc = wait_event_interruptible_timeout(opinfo->oplock_q, - opinfo->op_state == OPLOCK_STATE_NONE || - opinfo->op_state == OPLOCK_CLOSING, - OPLOCK_WAIT_TIME); - - /* 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; - } - - return false; -} - static bool oplock_break_set_ack_wait(struct oplock_info *opinfo) { bool ret = false; @@ -842,330 +816,492 @@ static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level, return opinfo->level > req_op_level; } -/** - * __smb2_oplock_break_noti() - send smb2 oplock break cmd from conn - * to client - * @wk: smb work object - * - * There are two ways this function can be called. 1- while file open we break - * from exclusive/batch lock to levelII oplock and 2- while file write/truncate - * we break from levelII oplock no oplock. - * work->request_buf contains oplock_info. - */ -static void __smb2_oplock_break_noti(struct work_struct *wk) +struct smb2_break_channel { + struct list_head list; + struct ksmbd_conn *conn; + struct ksmbd_tx_tracking tracking; + bool queued; + bool delivered; + bool failed; +}; + +static bool smb2_break_complete(struct oplock_info *opinfo, bool expect_ack) { - struct smb2_oplock_break *rsp = NULL; - struct ksmbd_work *work = container_of(wk, struct ksmbd_work, work); - struct ksmbd_conn *conn = work->conn; - struct oplock_break_info *br_info = work->request_buf; - struct smb2_hdr *rsp_hdr; - struct ksmbd_file *fp; + bool complete; - fp = ksmbd_lookup_global_fd(br_info->fid); - if (!fp) - goto out; + spin_lock(&opinfo->state_lock); + complete = opinfo->op_state == OPLOCK_CLOSING || + (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE); + spin_unlock(&opinfo->state_lock); + return complete; +} - if (allocate_interim_rsp_buf(work)) { - pr_err("smb2_allocate_rsp_buf failed! "); - ksmbd_fd_put(work, fp); - goto out; +static int smb2_break_failed(struct oplock_info *opinfo, bool expect_ack) +{ + spin_lock(&opinfo->state_lock); + /* An ACK or close which won the race has already completed the break. */ + if (opinfo->op_state == OPLOCK_CLOSING || + (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) { + spin_unlock(&opinfo->state_lock); + return 0; } + if (opinfo->is_lease) { + opinfo->o_lease->state = SMB2_LEASE_NONE_LE; + opinfo->o_lease->new_state = SMB2_LEASE_NONE_LE; + lease_update_oplock_levels(opinfo->o_lease); + atomic_set(&opinfo->breaking_cnt, 0); + } + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + opinfo->op_state = OPLOCK_STATE_NONE; + spin_unlock(&opinfo->state_lock); + wake_up_interruptible_all(&opinfo->oplock_q); + wake_up_interruptible_all(&opinfo->oplock_brk); + return ksmbd_invalidate_durable_fd(opinfo->fid); +} - rsp_hdr = smb_get_msg(work->response_buf); - memset(rsp_hdr, 0, sizeof(struct smb2_hdr) + 2); - rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER; - rsp_hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE; - rsp_hdr->CreditRequest = cpu_to_le16(0); - rsp_hdr->Command = SMB2_OPLOCK_BREAK; - rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR); - rsp_hdr->NextCommand = 0; - rsp_hdr->MessageId = cpu_to_le64(-1); - rsp_hdr->Id.SyncId.ProcessId = 0; - rsp_hdr->Id.SyncId.TreeId = 0; - rsp_hdr->SessionId = 0; - memset(rsp_hdr->Signature, 0, 16); +static int smb2_break_add_channel(struct list_head *head, struct ksmbd_conn *conn) +{ + struct smb2_break_channel *channel; - rsp = smb_get_msg(work->response_buf); - - rsp->StructureSize = cpu_to_le16(24); - if (!br_info->open_trunc && - (br_info->level == SMB2_OPLOCK_LEVEL_BATCH || - br_info->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE)) - rsp->OplockLevel = SMB2_OPLOCK_LEVEL_II; - else - rsp->OplockLevel = SMB2_OPLOCK_LEVEL_NONE; - rsp->Reserved = 0; - rsp->Reserved2 = 0; - rsp->PersistentFid = fp->persistent_id; - rsp->VolatileFid = fp->volatile_id; - - ksmbd_fd_put(work, fp); - if (ksmbd_iov_pin_rsp(work, (void *)rsp, - sizeof(struct smb2_oplock_break))) - goto out; - - ksmbd_debug(OPLOCK, - "sending oplock break v_id %llu p_id = %llu lock level = %d\n", - rsp->VolatileFid, rsp->PersistentFid, rsp->OplockLevel); - - ksmbd_conn_write(work); - -out: - ksmbd_free_work_struct(work); - ksmbd_conn_r_count_dec(conn); - ksmbd_conn_put(conn); + channel = kzalloc_obj(*channel, KSMBD_DEFAULT_GFP); + if (!channel) + return -ENOMEM; + channel->conn = ksmbd_conn_get(conn); + list_add_tail(&channel->list, head); + return 0; } /* - * Select and pin the connection used for an oplock break before doing any - * allocations which may sleep. The caller of oplock_break() holds a live - * reference on ci (a file being opened, a file being operated on, or an - * explicit ksmbd_inode_lookup_lock() reference in the parent lease break - * paths), so the inode cannot be freed during the call and its lock is - * reachable without dereferencing opinfo->o_fp, which is not pinned by - * the oplock reference and may be freed by a concurrent close. - * - * opinfo->conn is cleared under ci->m_lock by session_fd_check() when the - * durable handle owning the oplock is disconnected, reassigned by - * ksmbd_reopen_durable_fd() under the same lock, and the last - * ksmbd_conn_put() of the old connection frees it. Holding the read lock - * excludes both writers, so the connection cannot be freed while it is - * selected. + * Snapshot connections before sending. References keep their transports alive, + * while r_count is taken separately for each write, under request_lock. */ -static struct ksmbd_conn *smb2_oplock_break_conn_get(struct oplock_info *opinfo, - struct ksmbd_inode *ci) +static int smb2_break_collect_channels(struct oplock_info *opinfo, + struct ksmbd_inode *ci, + struct smb2_break_channel *owner, + struct list_head *head, + struct smb2_oplock_break *rsp) { + struct ksmbd_session *sess = NULL; + struct ksmbd_conn *session_conns[KSMBD_MAX_CHANNELS]; + struct ksmbd_file_table *ft; + struct ksmbd_file *fp; struct ksmbd_conn *conn; + struct lease_table *lb; + char guid[SMB2_CLIENT_GUID_SIZE]; + u64 session_id = 0; + unsigned int nr_conns, i; + bool have_guid = false; + int bkt, ret = 0; + /* m_lock protects membership and binding, but not the session's lifetime. */ down_read(&ci->m_lock); - conn = READ_ONCE(opinfo->conn); - if (conn && !ksmbd_conn_releasing(conn)) - conn = ksmbd_conn_get(conn); - else - conn = NULL; + if (!list_empty(&opinfo->op_entry)) { + if (!opinfo->is_lease) { + rsp->PersistentFid = READ_ONCE(opinfo->o_fp->persistent_id); + rsp->VolatileFid = READ_ONCE(opinfo->o_fp->volatile_id); + if (!has_file_id(rsp->PersistentFid) || + !has_file_id(rsp->VolatileFid)) { + /* A detached oplock must take the failed-break path. */ + ret = opinfo->conn ? -ENOENT : -ENOTCONN; + goto unlock; + } + session_id = opinfo->session_id; + } + conn = opinfo->conn; + if (conn) { + owner->conn = ksmbd_conn_get(conn); + memcpy(guid, conn->ClientGUID, sizeof(guid)); + have_guid = true; + } + } else { + ret = -ENOENT; + } +unlock: up_read(&ci->m_lock); + if (ret) + return ret; - return conn; + if (opinfo->is_lease && !have_guid) { + read_lock(&lease_list_lock); + lb = opinfo->o_lease->l_lb; + if (lb) { + memcpy(guid, lb->client_guid, sizeof(guid)); + have_guid = true; + if (lb->conn) + owner->conn = ksmbd_conn_get(lb->conn); + } + read_unlock(&lease_list_lock); + } + if (owner->conn) + list_add_tail(&owner->list, head); + + if (!opinfo->is_lease) { + /* Session teardown takes m_lock while holding the session table lock. */ + sess = ksmbd_session_lookup_slowpath(session_id); + if (!sess) + return -ENOENT; + + /* Revalidate the binding and IDs after taking a safe session reference. */ + down_read(&ci->m_lock); + if (list_empty(&opinfo->op_entry) || opinfo->sess != sess) { + ret = -ENOENT; + } else { + ft = &sess->file_table; + read_lock(&ft->lock); + fp = opinfo->o_fp; + if (fp->f_state == FP_CLOSED || + !has_file_id(fp->volatile_id) || + idr_find(ft->idr, fp->volatile_id) != fp) { + ret = -ENOENT; + } else { + rsp->PersistentFid = READ_ONCE(fp->persistent_id); + rsp->VolatileFid = fp->volatile_id; + } + read_unlock(&ft->lock); + } + up_read(&ci->m_lock); + if (ret) { + ksmbd_user_session_put(sess); + return ret; + } + } + + if (opinfo->is_lease && have_guid) { + down_read(&conn_list_lock); + hash_for_each(conn_list, bkt, conn, hlist) { + if (conn == owner->conn || !ksmbd_conn_good(conn) || + memcmp(conn->ClientGUID, guid, sizeof(guid))) + continue; + ret = smb2_break_add_channel(head, conn); + if (ret) + break; + } + up_read(&conn_list_lock); + } else if (!opinfo->is_lease && sess && sess->dialect >= SMB30_PROT_ID) { + nr_conns = ksmbd_session_get_channels(sess, session_conns, + ARRAY_SIZE(session_conns)); + for (i = 0; i < nr_conns; i++) { + conn = session_conns[i]; + if (!ret && conn && conn != owner->conn && ksmbd_conn_good(conn)) + ret = smb2_break_add_channel(head, conn); + /* Drop all snapshot references, also on allocation failure. */ + ksmbd_conn_put(conn); + } + } + ksmbd_user_session_put(sess); + + /* Allocation pressure may limit retries, but must not discard the owner. */ + return list_empty(head) ? (ret ?: -ENOTCONN) : 0; } -/** - * smb2_oplock_break_noti() - send smb2 exclusive/batch to level2 oplock - * break command from server to client - * @opinfo: oplock info object - * @ci: inode owning the break target's oplock list, pinned by - * the caller - * - * Return: 0 on success, otherwise error - */ -static int smb2_oplock_break_noti(struct oplock_info *opinfo, - struct ksmbd_inode *ci) +static bool smb2_break_delivered(struct list_head *head, bool require_live) { + struct smb2_break_channel *channel; struct ksmbd_conn *conn; - struct oplock_break_info *br_info; - int ret = 0; + + list_for_each_entry(channel, head, list) { + conn = channel->conn; + if (!channel->queued || (require_live && !ksmbd_conn_good(conn))) + continue; + /* A synchronous send needs no separate delivery probe. */ + if (channel->delivered || !channel->tracking.valid || + conn->transport->ops->is_acked(conn->transport, + &channel->tracking)) { + channel->delivered = true; + return true; + } + } + return false; +} + +static bool smb2_break_has_unsent_channels(struct list_head *head) +{ + struct smb2_break_channel *channel; + + list_for_each_entry(channel, head, list) { + if (!channel->queued && !channel->failed) + return true; + } + return false; +} + +static int smb2_break_wait(struct oplock_info *opinfo, struct list_head *head, + struct smb2_break_channel *sent_channel, + bool expect_ack, bool wait_ack, bool can_retry, + unsigned long retry_at, unsigned long deadline) +{ + unsigned long remaining; + long ret; + + for (;;) { + if (smb2_break_complete(opinfo, expect_ack)) + return 0; + if (!wait_ack && smb2_break_delivered(head, false)) + return 0; + if (time_after_eq(jiffies, deadline)) + return -ETIMEDOUT; + /* TCP delivery followed by a slow SMB ACK is not a send failure. */ + if (can_retry && !smb2_break_delivered(head, true) && + ((sent_channel && !ksmbd_conn_good(sent_channel->conn)) || + time_after_eq(jiffies, retry_at))) + return -EAGAIN; + + remaining = min_t(unsigned long, deadline - jiffies, + max_t(unsigned long, 1, HZ / 10)); + ret = wait_event_interruptible_timeout(opinfo->oplock_q, + smb2_break_complete(opinfo, expect_ack), + remaining); + if (ret < 0) + return ret; + } +} + +static void smb2_break_init_header(struct smb2_hdr *hdr) +{ + memset(hdr, 0, sizeof(*hdr)); + hdr->ProtocolId = SMB2_PROTO_NUMBER; + hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE; + hdr->Command = SMB2_OPLOCK_BREAK; + hdr->Flags = SMB2_FLAGS_SERVER_TO_REDIR; + hdr->MessageId = cpu_to_le64(-1); +} + +/* Build one notification and reuse its payload and epoch on each connection. */ +static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci, + bool expect_ack, bool wait_ack, bool inc_epoch, + struct work_struct *interim_work) +{ + struct smb2_break_channel owner = {}, *channel, *tmp, *last = NULL; struct ksmbd_work *work; + struct smb2_oplock_break *oplock_rsp; + struct smb2_lease_break *lease_rsp; + struct lease *lease = opinfo->o_lease; + unsigned long deadline, retry_at; + int ret, size; + LIST_HEAD(channels); - conn = smb2_oplock_break_conn_get(opinfo, ci); - if (!conn) - return ksmbd_invalidate_durable_fd(opinfo->fid); - + if (smb2_break_complete(opinfo, expect_ack)) { + ret = 0; + goto out_interim; + } + wait_ack = wait_ack && expect_ack; work = ksmbd_alloc_work_struct(); if (!work) { - ksmbd_conn_put(conn); - return -ENOMEM; + ret = smb2_break_failed(opinfo, expect_ack); + goto out_interim; } + ret = allocate_interim_rsp_buf(work); + if (ret) + goto failed; + oplock_rsp = smb_get_msg(work->response_buf); + smb2_break_init_header(&oplock_rsp->hdr); - br_info = kmalloc_obj(struct oplock_break_info, KSMBD_DEFAULT_GFP); - if (!br_info) { - ksmbd_free_work_struct(work); - ksmbd_conn_put(conn); - return -ENOMEM; + ret = smb2_break_collect_channels(opinfo, ci, &owner, &channels, oplock_rsp); + if (ret == -ENOENT) + goto out; + if (ret) + goto failed; + + spin_lock(&opinfo->state_lock); + if (opinfo->op_state == OPLOCK_CLOSING || + (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) { + spin_unlock(&opinfo->state_lock); + ret = 0; + goto out; } - - br_info->level = opinfo->level; - br_info->fid = opinfo->fid; - br_info->open_trunc = opinfo->open_trunc; - - work->request_buf = (char *)br_info; - /* Transfer the reference acquired by smb2_oplock_break_conn_get(). */ - work->conn = conn; - work->sess = opinfo->sess; - - ksmbd_conn_r_count_inc(conn); - if (opinfo->op_state == OPLOCK_ACK_WAIT) { - INIT_WORK(&work->work, __smb2_oplock_break_noti); - ksmbd_queue_work(work); - - if (wait_for_break_ack(opinfo)) - ret = ksmbd_invalidate_durable_fd(opinfo->fid); + if (opinfo->is_lease) { + lease_rsp = smb_get_msg(work->response_buf); + lease_rsp->StructureSize = cpu_to_le16(44); + if (lease->version == 2) { + if (inc_epoch) + lease->epoch++; + lease_rsp->Epoch = cpu_to_le16(lease->epoch); + } + if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE)) + lease_rsp->Flags = SMB2_NOTIFY_BREAK_LEASE_FLAG_ACK_REQUIRED; + memcpy(lease_rsp->LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE); + lease_rsp->CurrentLeaseState = lease->state; + lease_rsp->NewLeaseState = lease->new_state; + size = sizeof(*lease_rsp); } else { - __smb2_oplock_break_noti(&work->work); - if (opinfo->level == SMB2_OPLOCK_LEVEL_II) + oplock_rsp->StructureSize = cpu_to_le16(24); + oplock_rsp->OplockLevel = !opinfo->open_trunc && + (opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || + opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) ? + SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE; + /* + * A level II break needs no ACK. Publish NONE before sending so + * an unsolicited ACK cannot observe II while delivery is pending. + */ + if (!expect_ack) opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + size = sizeof(*oplock_rsp); } + spin_unlock(&opinfo->state_lock); + ret = ksmbd_iov_pin_rsp(work, oplock_rsp, size); + if (ret) + goto failed; + + deadline = jiffies + OPLOCK_WAIT_TIME; +retry: + list_for_each_entry(channel, &channels, list) { + if (channel->queued || channel->failed) + continue; + if (smb2_break_complete(opinfo, expect_ack)) { + ret = 0; + goto out; + } + if (time_after_eq(jiffies, deadline)) { + ret = -ETIMEDOUT; + break; + } + if (!ksmbd_conn_r_count_inc_if_good(channel->conn)) { + channel->failed = true; + ret = -ENOTCONN; + continue; + } + work->conn = channel->conn; + ret = ksmbd_conn_write_tracked(work, &channel->tracking); + ksmbd_conn_r_count_dec(channel->conn); + if (ret) { + /* Only a nonblocking send guarantees no bytes on -EAGAIN. */ + if (ret != -EAGAIN || !channel->conn->transport->ops->is_acked) + channel->failed = true; + continue; + } + channel->queued = true; + if (interim_work) { + ksmbd_queue_io_work(interim_work); + interim_work = NULL; + } + last = channel; + retry_at = jiffies + channel->tracking.retry_after; + ret = smb2_break_wait(opinfo, &channels, channel, expect_ack, + wait_ack, smb2_break_has_unsent_channels(&channels), + retry_at, deadline); + if (ret != -EAGAIN) + goto done; + } + if (smb2_break_has_unsent_channels(&channels)) { + /* Allow busy writers to make progress before another nonblocking pass. */ + retry_at = jiffies + max_t(unsigned long, 1, HZ / 10); + ret = smb2_break_wait(opinfo, &channels, NULL, expect_ack, + wait_ack, true, retry_at, deadline); + if (ret == -EAGAIN) + goto retry; + } else if (last) { + /* Give already queued notifications the rest of the shared wait. */ + ret = smb2_break_wait(opinfo, &channels, last, expect_ack, + wait_ack, false, deadline, deadline); + } + +done: + if (ret) + goto failed; + if (!expect_ack && opinfo->is_lease) { + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_CLOSING) { + lease->state = lease->new_state; + lease_update_oplock_levels(lease); + } + spin_unlock(&opinfo->state_lock); + } + goto out; +failed: + ret = smb2_break_failed(opinfo, expect_ack); +out: + ksmbd_free_work_struct(work); + list_for_each_entry_safe(channel, tmp, &channels, list) { + list_del(&channel->list); + ksmbd_conn_put(channel->conn); + if (channel != &owner) + kfree(channel); + } +out_interim: + if (interim_work) + ksmbd_queue_io_work(interim_work); return ret; } -/** - * __smb2_lease_break_noti() - send lease break command from server - * to client - * @wk: smb work object - */ -static void __smb2_lease_break_noti(struct work_struct *wk) +struct smb2_interim_work { + struct work_struct work; + struct ksmbd_work *in_work; +}; + +static void smb2_break_interim_work(struct work_struct *work) { - struct smb2_lease_break *rsp = NULL; - struct ksmbd_work *work = container_of(wk, struct ksmbd_work, work); - struct ksmbd_conn *conn = work->conn; - struct lease_break_info *br_info = work->request_buf; - struct smb2_hdr *rsp_hdr; + struct smb2_interim_work *iw = container_of(work, struct smb2_interim_work, work); - if (allocate_interim_rsp_buf(work)) { - ksmbd_debug(OPLOCK, "smb2_allocate_rsp_buf failed! "); - goto out; + if (!setup_async_work(iw->in_work, NULL, NULL)) { + smb2_send_interim_resp(iw->in_work, STATUS_PENDING); + release_async_work(iw->in_work); } - - rsp_hdr = smb_get_msg(work->response_buf); - memset(rsp_hdr, 0, sizeof(struct smb2_hdr) + 2); - rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER; - rsp_hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE; - rsp_hdr->CreditRequest = cpu_to_le16(0); - rsp_hdr->Command = SMB2_OPLOCK_BREAK; - rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR); - rsp_hdr->NextCommand = 0; - rsp_hdr->MessageId = cpu_to_le64(-1); - rsp_hdr->Id.SyncId.ProcessId = 0; - rsp_hdr->Id.SyncId.TreeId = 0; - rsp_hdr->SessionId = 0; - memset(rsp_hdr->Signature, 0, 16); - - rsp = smb_get_msg(work->response_buf); - rsp->StructureSize = cpu_to_le16(44); - rsp->Epoch = br_info->epoch; - rsp->Flags = 0; - - if (br_info->curr_state & (SMB2_LEASE_WRITE_CACHING_LE | - SMB2_LEASE_HANDLE_CACHING_LE)) - rsp->Flags = SMB2_NOTIFY_BREAK_LEASE_FLAG_ACK_REQUIRED; - - memcpy(rsp->LeaseKey, br_info->lease_key, SMB2_LEASE_KEY_SIZE); - rsp->CurrentLeaseState = br_info->curr_state; - rsp->NewLeaseState = br_info->new_state; - rsp->BreakReason = 0; - rsp->AccessMaskHint = 0; - rsp->ShareMaskHint = 0; - - if (ksmbd_iov_pin_rsp(work, (void *)rsp, - sizeof(struct smb2_lease_break))) - goto out; - - ksmbd_conn_write(work); - -out: - ksmbd_free_work_struct(work); - ksmbd_conn_r_count_dec(conn); - 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 smb2_break_work { + struct work_struct work; + struct oplock_info *opinfo; + struct ksmbd_inode *ci; + bool expect_ack; + bool wait_ack; + bool inc_epoch; + int ret; +}; + +static void smb2_break_notify_work(struct work_struct *work) { - struct lease *lease = opinfo->o_lease; - struct lease_table *lb; - struct ksmbd_conn *conn; + struct smb2_break_work *bw = container_of(work, struct smb2_break_work, work); - /* 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; + bw->ret = smb2_break_notify(bw->opinfo, bw->ci, bw->expect_ack, + bw->wait_ack, bw->inc_epoch, NULL); } -/** - * smb2_lease_break_noti() - break lease when a new client request - * write lease - * @opinfo: contains lease state information - * @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 sync, - bool inc_epoch) +static int smb2_break_notify_with_interim(struct oplock_info *opinfo, + struct ksmbd_inode *ci, + bool expect_ack, bool wait_ack, + bool inc_epoch, struct ksmbd_work *in_work, + bool notify_first) { - struct ksmbd_conn *conn; - struct ksmbd_work *work; - struct lease_break_info *br_info; - struct lease *lease = opinfo->o_lease; + struct smb2_break_work bw = { + .opinfo = opinfo, + .ci = ci, + .expect_ack = expect_ack, + .wait_ack = wait_ack, + .inc_epoch = inc_epoch, + }; - conn = smb2_lease_break_conn_get(opinfo); - if (!conn) - return ksmbd_invalidate_durable_fd(opinfo->fid); + if (!in_work) + return smb2_break_notify(opinfo, ci, expect_ack, wait_ack, inc_epoch, NULL); - work = ksmbd_alloc_work_struct(); - if (!work) { - ksmbd_conn_put(conn); - return -ENOMEM; + if (notify_first) { + struct smb2_interim_work iw = { .in_work = in_work }; + int ret; + + /* + * Rename sends the first notification in the request context. + * Queue the interim writer after that send so a blocked response + * cannot stop notification retries or the break ACK wait. + */ + INIT_WORK_ONSTACK(&iw.work, smb2_break_interim_work); + ret = smb2_break_notify(opinfo, ci, expect_ack, wait_ack, + inc_epoch, &iw.work); + flush_work(&iw.work); + destroy_work_on_stack(&iw.work); + return ret; } - 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; + /* A blocked interim writer must not stop notification sends or retries. */ + INIT_WORK_ONSTACK(&bw.work, smb2_break_notify_work); + ksmbd_queue_break_work(&bw.work); + if (!setup_async_work(in_work, NULL, NULL)) { + smb2_send_interim_resp(in_work, STATUS_PENDING); + release_async_work(in_work); } - - br_info->curr_state = lease->state; - br_info->new_state = lease->new_state; - if (lease->version == 2) { - if (inc_epoch) - lease->epoch++; - br_info->epoch = cpu_to_le16(lease->epoch); - } else { - br_info->epoch = 0; - } - memcpy(br_info->lease_key, lease->lease_key, SMB2_LEASE_KEY_SIZE); - - work->request_buf = (char *)br_info; - /* Transfer the reference acquired by smb2_lease_break_conn_get(). */ - work->conn = conn; - work->sess = opinfo->sess; - - ksmbd_conn_r_count_inc(conn); - if (opinfo->op_state == OPLOCK_ACK_WAIT) { - 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); - if (opinfo->o_lease->new_state == SMB2_LEASE_NONE_LE) { - opinfo->o_lease->state = SMB2_LEASE_NONE_LE; - lease_update_oplock_levels(opinfo->o_lease); - } - } - return 0; + /* The caller keeps opinfo and ci alive until the notifier has finished. */ + flush_work(&bw.work); + destroy_work_on_stack(&bw.work); + return bw.ret; } static void wait_lease_breaking(struct oplock_info *opinfo) @@ -1187,10 +1323,11 @@ static void wait_lease_breaking(struct oplock_info *opinfo) static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, int req_op_level, struct ksmbd_work *in_work, - bool share_break, bool sync_lease_break) + bool share_break, bool notify_first) { int err = 0; bool sent_interim = false; + bool expect_ack = false; /* Need to break exclusive/batch oplock, write lease or overwrite_if */ ksmbd_debug(OPLOCK, @@ -1248,8 +1385,9 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, } } - if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE | - SMB2_LEASE_HANDLE_CACHING_LE)) { + expect_ack = lease->state & (SMB2_LEASE_WRITE_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE); + if (expect_ack) { if (!oplock_break_set_ack_wait(brk_opinfo)) { atomic_dec_if_positive(&brk_opinfo->breaking_cnt); clear_and_wake_up_bit(0, &brk_opinfo->pending_break); @@ -1265,16 +1403,12 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, inc_epoch = false; lease->reuse_epoch = false; } - err = smb2_lease_break_noti(brk_opinfo, sync_lease_break, inc_epoch); + err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack, + wait_ack, inc_epoch, + sent_interim ? NULL : in_work, + notify_first); + sent_interim = true; 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) @@ -1310,26 +1444,16 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { + expect_ack = true; if (!oplock_break_set_ack_wait(brk_opinfo)) { clear_and_wake_up_bit(0, &brk_opinfo->pending_break); return -ENOENT; } } - - /* - * 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, ci); + err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack, + true, false, in_work, false); ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) @@ -1491,7 +1615,7 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, down_read(&p_ci->m_lock); list_for_each_entry(opinfo, &p_ci->m_op_list, op_entry) { - if (opinfo->conn == NULL || !opinfo->is_lease) + if (!opinfo->is_lease) continue; if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE && @@ -1502,11 +1626,6 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, if (!atomic_inc_not_zero(&opinfo->refcount)) continue; - if (ksmbd_conn_releasing(opinfo->conn)) { - opinfo_put(opinfo); - continue; - } - if (oplock_break_add(&brk_list, opinfo)) opinfo_put(opinfo); } @@ -1539,18 +1658,13 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp) down_read(&p_ci->m_lock); list_for_each_entry(opinfo, &p_ci->m_op_list, op_entry) { - if (opinfo->conn == NULL || !opinfo->is_lease) + if (!opinfo->is_lease) continue; if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE) { if (!atomic_inc_not_zero(&opinfo->refcount)) continue; - if (ksmbd_conn_releasing(opinfo->conn)) { - opinfo_put(opinfo); - continue; - } - if (oplock_break_add(&brk_list, opinfo)) opinfo_put(opinfo); } @@ -1699,7 +1813,11 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, err = oplock_break(prev_opinfo, ci, break_level, work, share_ret < 0 && prev_opinfo->is_lease, false); - if (prev_durable_detached || (prev_durable_open && err == -ENOENT)) + /* Keep a detached durable lease if its break retains handle caching. */ + if ((prev_durable_detached && + (!prev_op_has_lease || + !(prev_opinfo->o_lease->state & SMB2_LEASE_HANDLE_CACHING_LE))) || + (prev_durable_open && err == -ENOENT)) ksmbd_invalidate_durable_fd(prev_fid); opinfo_put(prev_opinfo); if (err == -EAGAIN) { @@ -1803,8 +1921,7 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work, } brk_opinfo->open_trunc = is_trunc; - oplock_break(brk_opinfo, fp->f_ci, SMB2_OPLOCK_LEVEL_II, work, false, - false); + oplock_break(brk_opinfo, fp->f_ci, SMB2_OPLOCK_LEVEL_II, work, false, false); sent_break = true; opinfo_put(brk_opinfo); @@ -1819,12 +1936,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 + * @notify_first: send the lease break notification before the interim response */ 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 sync_lease_break) + bool notify_first) { struct oplock_info *op, *brk_op; struct oplock_break_entry *ent, *tmp; @@ -1842,17 +1959,9 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, down_read(&ci->m_lock); list_for_each_entry(brk_op, &ci->m_op_list, op_entry) { - if (brk_op->conn == NULL) - continue; - if (!atomic_inc_not_zero(&brk_op->refcount)) continue; - if (ksmbd_conn_releasing(brk_op->conn)) { - opinfo_put(brk_op); - continue; - } - if (!brk_op->is_lease && brk_op->level != SMB2_OPLOCK_LEVEL_II) { ksmbd_debug(OPLOCK, "unexpected oplock(0x%x)\n", @@ -1867,10 +1976,8 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, goto next; if (op && op->is_lease && brk_op->is_lease && - !memcmp(conn->ClientGUID, brk_op->conn->ClientGUID, - SMB2_CLIENT_GUID_SIZE) && - !memcmp(op->o_lease->lease_key, brk_op->o_lease->lease_key, - SMB2_LEASE_KEY_SIZE)) + compare_guid_key(brk_op, conn->ClientGUID, + op->o_lease->lease_key)) goto next; brk_op->open_trunc = is_trunc; @@ -1900,7 +2007,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, sync_lease_break); + false, notify_first); } sent_interim = true; list_del(&ent->list); diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index b08d21758e07..e114e2148c30 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -62,6 +62,7 @@ struct lease { struct oplock_info { struct ksmbd_conn *conn; struct ksmbd_session *sess; + u64 session_id; struct ksmbd_work *work; struct ksmbd_file *o_fp; int level; @@ -82,19 +83,6 @@ struct oplock_info { struct rcu_head rcu; }; -struct lease_break_info { - __le32 curr_state; - __le32 new_state; - __le16 epoch; - char lease_key[SMB2_LEASE_KEY_SIZE]; -}; - -struct oplock_break_info { - int level; - int open_trunc; - int fid; -}; - 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, bool replay); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 496055ce4a4d..11c123673f9a 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1767,6 +1767,7 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, ksmbd_conn_put(op->conn); op->conn = NULL; op->sess = NULL; + op->session_id = 0; } up_write(&ci->m_lock); @@ -1953,6 +1954,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) continue; op->conn = ksmbd_conn_get(fp->conn); op->sess = work->sess; + op->session_id = work->sess->id; } up_write(&ci->m_lock); From 20e3068315423e6e493588dd94d462c8e7fd3a2e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 5 Oct 2026 11:20:33 +0900 Subject: [PATCH 57/95] ksmbd: keep lease tables until their last lease is closed Disconnecting one channel destroys the lease table for its ClientGuid, even when another channel still uses the same opens. A lease break ACK on the surviving channel can no longer find the lease. Detached durable opens also lose their lease lookup entries before reconnect or expiry. Tie a client table to its lease entries instead of the connection which created it. Remove the table when its last lease is removed, and release any remaining entries at server shutdown. Do not destroy client tables from connection teardown or keep a connection reference in each table. For a detached lease, collect break candidates using the table's ClientGuid directly. The existing notification retry path takes its own references on matching live connections. Fixes: 0626e6641f6b ("cifsd: add server handler for central processing and tranport layers") Reported-by: Pan Guihua Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 46 +++++++++++++++++------------------------- fs/smb/server/oplock.h | 3 +-- fs/smb/server/server.c | 3 +-- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index eedbbf9b05d0..9111cf727dd9 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -125,6 +125,12 @@ static void lease_add_table(struct lease *lease, struct lease_table *lb) spin_unlock(&lb->lb_lock); } +static void free_lease_table(struct lease_table *lb) +{ + kfree(lb); +} + +/* The caller holds lease_list_lock for write. */ static void lease_del_table(struct lease *lease) { struct lease_table *lb = lease->l_lb; @@ -143,6 +149,10 @@ static void lease_del_table(struct lease *lease) spin_unlock(&lb->lb_lock); lease_put(lease); + if (list_empty(&lb->lease_list)) { + list_del(&lb->l_entry); + free_lease_table(lb); + } } static struct lease_table *alloc_lease_table(struct oplock_info *opinfo) @@ -155,21 +165,11 @@ static struct lease_table *alloc_lease_table(struct oplock_info *opinfo) memcpy(lb->client_guid, opinfo->conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); - lb->conn = ksmbd_conn_get(opinfo->conn); INIT_LIST_HEAD(&lb->lease_list); spin_lock_init(&lb->lb_lock); return lb; } -static void free_lease_table(struct lease_table *lb) -{ - if (!lb) - return; - - ksmbd_conn_put(lb->conn); - kfree(lb); -} - static struct lease *alloc_lease(struct lease_ctx_info *lctx, struct ksmbd_inode *ci) { @@ -927,8 +927,6 @@ static int smb2_break_collect_channels(struct oplock_info *opinfo, if (lb) { memcpy(guid, lb->client_guid, sizeof(guid)); have_guid = true; - if (lb->conn) - owner->conn = ksmbd_conn_get(lb->conn); } read_unlock(&lease_list_lock); } @@ -1495,25 +1493,17 @@ static void oplock_break_drain_none(struct list_head *head, } } -void destroy_lease_table(struct ksmbd_conn *conn) +void destroy_lease_table(void) { - struct lease_table *lb, *lbtmp; - struct lease *lease, *ltmp; + struct lease_table *lb; + struct lease *lease; write_lock(&lease_list_lock); - if (list_empty(&lease_table_list)) { - write_unlock(&lease_list_lock); - return; - } - - list_for_each_entry_safe(lb, lbtmp, &lease_table_list, l_entry) { - if (conn && memcmp(lb->client_guid, conn->ClientGUID, - SMB2_CLIENT_GUID_SIZE)) - continue; - list_for_each_entry_safe(lease, ltmp, &lb->lease_list, l_entry) - lease_del_table(lease); - list_del(&lb->l_entry); - free_lease_table(lb); + while (!list_empty(&lease_table_list)) { + lb = list_first_entry(&lease_table_list, struct lease_table, + l_entry); + lease = list_first_entry(&lb->lease_list, struct lease, l_entry); + lease_del_table(lease); } write_unlock(&lease_list_lock); } diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index e114e2148c30..5ef05166e502 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -34,7 +34,6 @@ struct lease_ctx_info { struct lease_table { char client_guid[SMB2_CLIENT_GUID_SIZE]; - struct ksmbd_conn *conn; struct list_head lease_list; struct list_head l_entry; spinlock_t lb_lock; @@ -122,7 +121,7 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, char *lease_key); int find_same_lease_key(struct ksmbd_conn *conn, struct ksmbd_inode *ci, struct lease_ctx_info *lctx); -void destroy_lease_table(struct ksmbd_conn *conn); +void destroy_lease_table(void); void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, struct lease_ctx_info *lctx); void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp); diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 1cb995a36b21..effc539548dc 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -436,7 +436,6 @@ static int ksmbd_server_process_request(struct ksmbd_conn *conn) static int ksmbd_server_terminate_conn(struct ksmbd_conn *conn) { ksmbd_conn_sessions_cleanup(conn); - destroy_lease_table(conn); return 0; } @@ -664,7 +663,7 @@ static int ksmbd_server_shutdown(void) ksmbd_proc_cleanup(); ksmbd_crypto_destroy(); ksmbd_free_global_file_table(); - destroy_lease_table(NULL); + destroy_lease_table(); ksmbd_work_pool_destroy(); ksmbd_exit_file_cache(); server_conf_free(); From 923535fe6e6c2d13f6f22ae696559ea4ea29d0ba Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 5 Oct 2026 11:25:08 +0900 Subject: [PATCH 58/95] ksmbd: index byte-range locks independently of their connection SMB locks belong to an open, but the lock conflict and UNLOCK scan only visits connections in conn_list. When a channel is removed, its locks remain attached to live opens and become invisible to another channel. An exact UNLOCK returns STATUS_RANGE_NOT_LOCKED, and zero-byte lock conflicts are no longer detected. A request finishing on the old channel after it is removed can publish another invisible lock. Index granted SMB locks by VFS inode, with the open's lock_list retaining ownership. Protect lookup, publication and removal with one spinlock. Use the inode rather than the dentry-based ksmbd_inode so hard-link opens still participate in the same conflict checks. Keep these entries through durable detach and reconnect. Publish a batch only after the existing LOCK handler commits it, and remove every entry before final close drops the file reference. Drop the per-lock connection references and connection-local lists; channel changes no longer require moving lock entries or waiting for unrelated requests to finish. Prefer an exclusive lock when several locks on an open match an UNLOCK. This preserves stacked zero-byte lock behavior despite the inode hash inserting newer locks before older entries. Fixes: d63528eb0d43 ("ksmbd: free ksmbd_lock when file is closed") Reported-by: Pan Guihua Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 2 - fs/smb/server/connection.h | 2 - fs/smb/server/smb2pdu.c | 104 ++----------------------- fs/smb/server/vfs_cache.c | 150 ++++++++++++++++++++++++++----------- fs/smb/server/vfs_cache.h | 7 +- 5 files changed, 120 insertions(+), 145 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 19d32681bb92..0a4be9e423a3 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -327,8 +327,6 @@ struct ksmbd_conn *ksmbd_conn_alloc(void) ida_init(&conn->async_ida); xa_init(&conn->sessions); - spin_lock_init(&conn->llist_lock); - INIT_LIST_HEAD(&conn->lock_list); init_rwsem(&conn->session_lock); diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 85e2d2073cd2..53eb91f618b4 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -112,8 +112,6 @@ struct ksmbd_conn { char ClientGUID[SMB2_CLIENT_GUID_SIZE]; struct ntlmssp_auth ntlmssp; - spinlock_t llist_lock; - struct list_head lock_list; struct preauth_integrity_info *preauth_info; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 4278b0f853ae..16aec995da8f 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10271,7 +10271,7 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, lock->end = flock->fl_end; lock->flags = flags; lock->zero_len = zero_len; - INIT_LIST_HEAD(&lock->clist); + INIT_HLIST_NODE(&lock->hlist); INIT_LIST_HEAD(&lock->flist); INIT_LIST_HEAD(&lock->llist); list_add_tail(&lock->llist, lock_list); @@ -10300,12 +10300,6 @@ static void smb2_free_blocked_lock(struct file_lock *flock) locks_free_lock(flock); } -static inline bool lock_defer_pending(struct file_lock *fl) -{ - /* check pending lock waiters */ - return waitqueue_active(&fl->c.flc_wait); -} - /** * smb2_lock() - handler for smb2 file lock command * @work: smb work containing lock command buffer @@ -10325,12 +10319,10 @@ int smb2_lock(struct ksmbd_work *work) int cmd = 0; int err = -EIO, i, rc = 0; u64 lock_start, lock_length; - struct ksmbd_lock *smb_lock = NULL, *cmp_lock, *tmp, *tmp2; - struct ksmbd_conn *conn; - int nolock = 0; + struct ksmbd_lock *smb_lock = NULL, *cmp_lock, *tmp; LIST_HEAD(lock_list); LIST_HEAD(rollback_list); - int prior_lock = 0, bkt; + int prior_lock = 0; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; bool lock_replayed; @@ -10463,79 +10455,10 @@ int smb2_lock(struct ksmbd_work *work) !(smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY)) goto no_check_cl; - nolock = 1; - /* check locks in connection list */ - down_read(&conn_list_lock); - hash_for_each(conn_list, bkt, conn, hlist) { - spin_lock(&conn->llist_lock); - list_for_each_entry_safe(cmp_lock, tmp2, &conn->lock_list, clist) { - if (file_inode(cmp_lock->fl->c.flc_file) != - file_inode(smb_lock->fl->c.flc_file)) - continue; - - if (lock_is_unlock(smb_lock->fl)) { - if (cmp_lock->fl->c.flc_file == smb_lock->fl->c.flc_file && - cmp_lock->start == smb_lock->start && - cmp_lock->end == smb_lock->end && - !lock_defer_pending(cmp_lock->fl)) { - nolock = 0; - 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); - smb2_free_lock(cmp_lock->fl); - kfree(cmp_lock); - goto out_check_cl; - } - continue; - } - - if (cmp_lock->fl->c.flc_file == smb_lock->fl->c.flc_file) { - if (smb_lock->flags & SMB2_LOCKFLAG_SHARED) - continue; - } else { - if (cmp_lock->flags & SMB2_LOCKFLAG_SHARED) - continue; - } - - /* 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) { - spin_unlock(&conn->llist_lock); - up_read(&conn_list_lock); - pr_err("previous lock conflict with zero byte lock range\n"); - goto out; - } - - if (smb_lock->zero_len && !cmp_lock->zero_len && - smb_lock->start > cmp_lock->start && - 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->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); - pr_err("Not allow lock operation on exclusive lock range\n"); - goto out; - } - } - spin_unlock(&conn->llist_lock); - } - up_read(&conn_list_lock); -out_check_cl: - if (lock_is_unlock(smb_lock->fl) && nolock) { - pr_err("Try to unlock nolocked range\n"); - rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; + err = ksmbd_file_lock_check(smb_lock); + if (err) { + if (err == -ENOENT) + rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; goto out; } @@ -10649,18 +10572,7 @@ int smb2_lock(struct ksmbd_work *work) 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_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); - list_add_tail(&smb_lock->flist, - &fp->lock_list); - } - spin_unlock(&work->conn->llist_lock); - } + ksmbd_file_lock_add(fp, &rollback_list); if (!lock_replayed) smb2_update_lock_sequence(work, fp, req); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 11c123673f9a..459783910c04 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "glob.h" #include "vfs_cache.h" @@ -38,6 +39,10 @@ static struct ksmbd_file_table global_ft; static atomic_long_t fd_limit; static struct kmem_cache *filp_cache; +/* Both the inode hash and each open's lock_list use this lock. */ +static DEFINE_SPINLOCK(file_locks_lock); +static DEFINE_HASHTABLE(file_locks, 8); + static int ksmbd_mark_fp_closed(struct ksmbd_file *fp); #define OPLOCK_NONE 0 @@ -613,10 +618,109 @@ static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp write_unlock(&ft->lock); } +/** + * ksmbd_file_lock_check() - check conflicts or remove an exact unlock match + * @lock: unregistered lock from the current request + * + * Return: 0 on success, -ENOENT for an unmatched unlock, or -EAGAIN on conflict. + */ +int ksmbd_file_lock_check(struct ksmbd_lock *lock) +{ + struct inode *inode = file_inode(lock->fl->c.flc_file); + struct ksmbd_lock *cmp, *lock_to_remove = NULL; + int ret = lock_is_unlock(lock->fl) ? -ENOENT : 0; + + spin_lock(&file_locks_lock); + hash_for_each_possible(file_locks, cmp, hlist, (unsigned long)inode) { + if (file_inode(cmp->fl->c.flc_file) != inode) + continue; + + if (lock_is_unlock(lock->fl)) { + if (cmp->fl->c.flc_file != lock->fl->c.flc_file || + cmp->start != lock->start || cmp->end != lock->end) + continue; + if (waitqueue_active(&cmp->fl->c.flc_wait)) /* VFS waiters */ + continue; + + /* Prefer an exclusive lock over matching shared locks. */ + lock_to_remove = cmp; + if (cmp->flags & SMB2_LOCKFLAG_EXCLUSIVE) + break; + continue; + } + + if (cmp->fl->c.flc_file == lock->fl->c.flc_file) { + if (lock->flags & SMB2_LOCKFLAG_SHARED) + continue; + } else if (cmp->flags & SMB2_LOCKFLAG_SHARED) { + continue; + } + + if ((cmp->zero_len && !lock->zero_len && + cmp->start > lock->start && cmp->start <= lock->end) || + (lock->zero_len && !cmp->zero_len && + lock->start > cmp->start && lock->start <= cmp->end) || + (!cmp->zero_len && !lock->zero_len && + cmp->start <= lock->end && lock->start <= cmp->end)) { + ret = -EAGAIN; + break; + } + } + if (lock_to_remove) { + hash_del(&lock_to_remove->hlist); + list_del_init(&lock_to_remove->flist); + ret = 0; + } + spin_unlock(&file_locks_lock); + if (lock_to_remove) { + ksmbd_vfs_posix_lock_unblock(lock_to_remove->fl); + locks_free_lock(lock_to_remove->fl); + kfree(lock_to_remove); + } + return ret; +} + +/** + * ksmbd_file_lock_add() - publish a committed batch independently of its channel + * @fp: open holding the locks + * @locks: granted locks awaiting publication + */ +void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks) +{ + struct ksmbd_lock *lock, *tmp; + + spin_lock(&file_locks_lock); + list_for_each_entry_safe(lock, tmp, locks, llist) { + list_del_init(&lock->llist); + hash_add(file_locks, &lock->hlist, + (unsigned long)file_inode(fp->filp)); + list_add_tail(&lock->flist, &fp->lock_list); + } + spin_unlock(&file_locks_lock); +} + +static void ksmbd_file_lock_clear(struct ksmbd_file *fp) +{ + struct ksmbd_lock *lock, *tmp; + LIST_HEAD(dispose); + + spin_lock(&file_locks_lock); + list_for_each_entry(lock, &fp->lock_list, flist) + hash_del(&lock->hlist); + list_splice_init(&fp->lock_list, &dispose); + spin_unlock(&file_locks_lock); + + list_for_each_entry_safe(lock, tmp, &dispose, flist) { + list_del_init(&lock->flist); + ksmbd_vfs_posix_lock_unblock(lock->fl); + locks_free_lock(lock->fl); + kfree(lock); + } +} + static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) { struct file *filp; - struct ksmbd_lock *smb_lock, *tmp_lock; fd_limit_close(); ksmbd_remove_durable_fd(fp); @@ -626,31 +730,12 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) close_id_del_oplock(fp); filp = fp->filp; + /* Remove lookup entries before dropping the file's inode reference. */ + ksmbd_file_lock_clear(fp); __ksmbd_inode_close(fp); if (!IS_ERR_OR_NULL(filp)) fput(filp); - /* - * 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; - - if (conn) { - spin_lock(&conn->llist_lock); - list_del_init(&smb_lock->clist); - smb_lock->conn = NULL; - spin_unlock(&conn->llist_lock); - ksmbd_conn_put(conn); - } - - list_del_init(&smb_lock->flist); - ksmbd_vfs_posix_lock_unblock(smb_lock->fl); - locks_free_lock(smb_lock->fl); - kfree(smb_lock); - } - /* * Drop fp's strong reference on conn (taken in ksmbd_open_fd() / * ksmbd_reopen_durable_fd()). Durable fps that reached the @@ -1738,7 +1823,6 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, struct ksmbd_inode *ci; struct oplock_info *op; struct ksmbd_conn *conn; - struct ksmbd_lock *smb_lock, *tmp_lock; if (!is_reconnectable(fp)) return false; @@ -1771,18 +1855,6 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, } up_write(&ci->m_lock); - list_for_each_entry_safe(smb_lock, tmp_lock, &fp->lock_list, flist) { - struct ksmbd_conn *lock_conn = smb_lock->conn; - - if (!lock_conn) - continue; - spin_lock(&lock_conn->llist_lock); - list_del_init(&smb_lock->clist); - smb_lock->conn = NULL; - spin_unlock(&lock_conn->llist_lock); - ksmbd_conn_put(lock_conn); - } - spin_lock(&fp->f_lock); fp->conn = NULL; fp->tcon = NULL; @@ -1889,7 +1961,6 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) struct oplock_info *op; struct ksmbd_conn *conn = work->conn; struct ksmbd_share_config *old_share; - struct ksmbd_lock *smb_lock; unsigned int old_f_state; write_lock(&global_ft.lock); @@ -1939,13 +2010,6 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) return -EBADF; } - list_for_each_entry(smb_lock, &fp->lock_list, flist) { - smb_lock->conn = ksmbd_conn_get(conn); - spin_lock(&conn->llist_lock); - list_add_tail(&smb_lock->clist, &conn->lock_list); - spin_unlock(&conn->llist_lock); - } - ci = fp->f_ci; down_write(&ci->m_lock); list_for_each_entry_rcu(op, &ci->m_op_list, op_entry, diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 706633bb0419..6df48ae04548 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -34,11 +34,11 @@ struct ksmbd_conn; struct ksmbd_session; +struct ksmbd_file; struct ksmbd_lock { struct file_lock *fl; - struct ksmbd_conn *conn; - struct list_head clist; + struct hlist_node hlist; struct list_head flist; struct list_head llist; unsigned int flags; @@ -48,6 +48,9 @@ struct ksmbd_lock { unsigned long long end; }; +int ksmbd_file_lock_check(struct ksmbd_lock *lock); +void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks); + struct stream { char *name; ssize_t size; From 864bbcd5dc5e70d639a3011eae81a872b69ff402 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 5 Oct 2026 11:26:17 +0900 Subject: [PATCH 59/95] ksmbd: rebind completed CREATE replays without draining the old channel A completed DurableHandleReqV2 CREATE can be replayed through another channel while its original channel is exiting. The existing replay path returns the open but leaves its connection and oplock references pointing at the old channel. Its open count also stays on that channel, and a later CLOSE through the surviving channel decrements the wrong counter. After checking the owner, lease key and persistent flag, verify that an active open is still published in the requesting session's file table. Do this even when no oplock was granted, and before processing a consumed replay. Do not read a borrowed opinfo session pointer for this check. Keep a healthy original binding. If the original channel is exiting or releasing, move the open and its own oplock references to the requesting channel. Serialize this with CLOSE using the session file-table lock, and with detach, reconnect and notification snapshots using m_lock. The earlier lock index and lease-table changes make their lifetimes independent of this connection change. No old-channel r_count wait is needed, including when an unrelated request remains active there. Account final close against fp->conn in one place, balance durable detach and reconnect, and transfer the count with a replay binding change. A final global lookup put may run after CLOSE has removed the volatile id; unlink the open from the inode list on that final close as well. A live completed CREATE replay may follow a break that lowered the durable open's batch oplock. Apply the batch eligibility check only to detached reconnects, while retaining the owner, lease key and persistent flag checks for active replays. Fixes: bb8bf7eb13b5 ("ksmbd: add SMB3 request replay support") Reported-by: Pan Guihua Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 59 ++++---------- fs/smb/server/vfs_cache.c | 164 ++++++++++++++++++++++---------------- fs/smb/server/vfs_cache.h | 1 + 3 files changed, 112 insertions(+), 112 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 16aec995da8f..77823c3462c5 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3747,7 +3747,7 @@ static int smb2_check_durable_replay(struct ksmbd_work *work, struct oplock_info *opinfo; int ret = 0; - if (!fp->is_durable && !fp->is_persistent) + if (!fp->conn && !fp->is_durable && !fp->is_persistent) return -EACCES; if (ksmbd_vfs_compare_durable_owner(fp, work->sess->user) == false) @@ -3760,11 +3760,6 @@ static int smb2_check_durable_replay(struct ksmbd_work *work, 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, @@ -3778,7 +3773,9 @@ static int smb2_check_durable_replay(struct ksmbd_work *work, goto out; } - if (fp->is_durable && opinfo->level != SMB2_OPLOCK_LEVEL_BATCH) + /* A live CREATE replay may follow a completed oplock break. */ + if (!fp->conn && fp->is_durable && + opinfo->level != SMB2_OPLOCK_LEVEL_BATCH) ret = -EACCES; } out: @@ -3982,35 +3979,18 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - 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; - } + err = smb2_check_durable_replay(work, + dh_info->fp, + lc, + dh_info->persistent); + if (err) { + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + + err = ksmbd_rebind_replay_fd(work, dh_info->fp); + if (err) { ksmbd_put_durable_fd(dh_info->fp); - err = -EACCES; goto out; } @@ -4023,15 +4003,6 @@ static int parse_durable_handle_context(struct ksmbd_work *work, 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)) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 459783910c04..c5217433299e 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -610,7 +610,7 @@ static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp list_del_init(&fp->node); up_write(&fp->f_ci->m_lock); - if (!has_file_id(fp->volatile_id)) + if (!ft || !has_file_id(fp->volatile_id)) return; write_lock(&ft->lock); @@ -724,8 +724,7 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) fd_limit_close(); ksmbd_remove_durable_fd(fp); - if (ft) - __ksmbd_remove_fd(ft, fp); + __ksmbd_remove_fd(ft, fp); close_id_del_oplock(fp); filp = fp->filp; @@ -743,6 +742,7 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) * in which case there is nothing to drop here. */ if (fp->conn) { + atomic_dec(&fp->conn->stats.open_files_count); ksmbd_conn_put(fp->conn); fp->conn = NULL; } @@ -860,22 +860,7 @@ static struct ksmbd_file *__ksmbd_lookup_fd(struct ksmbd_file_table *ft, static void __put_fd_final(struct ksmbd_work *work, struct ksmbd_file *fp) { - /* - * Detached durable fp -- session_fd_check() cleared fp->conn at - * preserve, so this fp is no longer tracked by any conn's - * stats.open_files_count. This happens when - * ksmbd_scavenger_dispose_dh() hands the final close off to an - * m_fp_list walker (e.g. ksmbd_lookup_fd_inode()) whose work->conn - * is unrelated to the conn that originally opened the handle; close - * via the NULL-ft path so we do not underflow that unrelated - * counter. - */ - if (!fp->conn) { - __ksmbd_close_fd(NULL, fp); - return; - } - __ksmbd_close_fd(&work->sess->file_table, fp); - atomic_dec(&work->conn->stats.open_files_count); + __ksmbd_close_fd(fp->conn ? &work->sess->file_table : NULL, fp); } static void set_close_state_blocked_works(struct ksmbd_file *fp) @@ -1127,11 +1112,8 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work, up_write(&fp->f_ci->m_lock); *closed = true; - if (atomic_sub_and_test(n_to_drop, &fp->refcount)) { - if (fp->conn) - atomic_dec(&fp->conn->stats.open_files_count); + if (atomic_sub_and_test(n_to_drop, &fp->refcount)) __ksmbd_close_fd(NULL, fp); - } return 0; out: @@ -1421,7 +1403,7 @@ static int ksmbd_mark_fp_closed(struct ksmbd_file *fp) return 1; } -static int +static void __close_file_table_ids(struct ksmbd_session *sess, struct ksmbd_tree_connect *tcon, bool (*skip)(struct ksmbd_tree_connect *tcon, @@ -1432,7 +1414,6 @@ __close_file_table_ids(struct ksmbd_session *sess, struct ksmbd_file_table *ft = &sess->file_table; struct ksmbd_file *fp; unsigned int id = 0; - int num = 0; while (1) { int n_to_drop; @@ -1516,10 +1497,8 @@ __close_file_table_ids(struct ksmbd_session *sess, } /* - * fp->volatile_id is already cleared to prevent stale idr - * removal from a deferred final close. Remove fp from - * m_fp_list here because __ksmbd_remove_fd() will skip the - * list unlink when volatile_id is KSMBD_NO_FID. + * Stop inode walkers from finding the open after close is committed. + * Final close also unlinks fp if a global lookup owns the last put. */ down_write(&fp->f_ci->m_lock); list_del_init(&fp->node); @@ -1538,22 +1517,13 @@ __close_file_table_ids(struct ksmbd_session *sess, * the original reference until ksmbd_update_fstate() * observes the cleared volatile id. * - * If we end up as the final putter, finalize fp and - * account the open_files_count decrement via the caller's - * atomic_sub(num, ...). Otherwise the remaining user's - * ksmbd_fd_put() reaches __put_fd_final(), which does its - * own atomic_dec(&open_files_count), so we must not count - * this fp here -- doing so would double-decrement the - * connection-wide counter. + * The final close accounts the open against fp->conn, + * including when an in-flight user drops the last reference. */ - if (atomic_sub_and_test(n_to_drop, &fp->refcount)) { + if (atomic_sub_and_test(n_to_drop, &fp->refcount)) __ksmbd_close_fd(NULL, fp); - num++; - } id++; } - - return num; } static inline bool is_reconnectable(struct ksmbd_file *fp) @@ -1841,29 +1811,29 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, * / ksmbd_reopen_durable_fd()), so conn stays valid for the whole * body of this function regardless of any op->conn puts below. */ - conn = fp->conn; ci = fp->f_ci; down_write(&ci->m_lock); + conn = fp->conn; list_for_each_entry_rcu(op, &ci->m_op_list, op_entry, lockdep_is_held(&ci->m_lock)) { - if (op->conn != conn) + if (op->o_fp != fp) continue; ksmbd_conn_put(op->conn); op->conn = NULL; op->sess = NULL; op->session_id = 0; } - up_write(&ci->m_lock); - - spin_lock(&fp->f_lock); + write_lock(&global_ft.lock); fp->conn = NULL; fp->tcon = NULL; fp->volatile_id = KSMBD_NO_FID; - spin_unlock(&fp->f_lock); if (fp->durable_timeout) fp->durable_scavenger_timeout = jiffies_to_msecs(jiffies) + fp->durable_timeout; + atomic_dec(&conn->stats.open_files_count); + write_unlock(&global_ft.lock); + up_write(&ci->m_lock); /* Drop fp's own reference on conn. */ ksmbd_conn_put(conn); @@ -1872,22 +1842,12 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, void ksmbd_close_tree_conn_fds(struct ksmbd_work *work) { - int num = __close_file_table_ids(work->sess, - work->tcon, - tree_conn_fd_check, - false); - - atomic_sub(num, &work->conn->stats.open_files_count); + __close_file_table_ids(work->sess, work->tcon, tree_conn_fd_check, false); } void ksmbd_close_session_fds(struct ksmbd_work *work) { - int num = __close_file_table_ids(work->sess, - work->tcon, - session_fd_check, - true); - - atomic_sub(num, &work->conn->stats.open_files_count); + __close_file_table_ids(work->sess, work->tcon, session_fd_check, true); } int ksmbd_init_global_file_table(void) @@ -1957,23 +1917,26 @@ int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share, int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) { - struct ksmbd_inode *ci; + struct ksmbd_inode *ci = fp->f_ci; struct oplock_info *op; struct ksmbd_conn *conn = work->conn; struct ksmbd_share_config *old_share; unsigned int old_f_state; + int ret = -EBADF; + down_write(&ci->m_lock); write_lock(&global_ft.lock); - if ((!fp->is_durable && !fp->is_persistent) || fp->conn || fp->tcon) { + if ((!fp->is_durable && !fp->is_persistent) || fp->conn || fp->tcon || + fp->f_state != FP_INITED) { write_unlock(&global_ft.lock); pr_err("Invalid durable fd [%p:%p]\n", fp->conn, fp->tcon); - return -EBADF; + goto out; } if (has_file_id(fp->volatile_id)) { write_unlock(&global_ft.lock); pr_err("Still in use durable fd: %llu\n", fp->volatile_id); - return -EBADF; + goto out; } /* @@ -1993,6 +1956,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) fp->conn = ksmbd_conn_get(conn); fp->tcon = work->tcon; spin_unlock(&fp->f_lock); + atomic_inc(&conn->stats.open_files_count); write_unlock(&global_ft.lock); __open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID); @@ -2004,14 +1968,13 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) fp->share_conf = old_share; fp->f_state = old_f_state; spin_unlock(&fp->f_lock); + atomic_dec(&conn->stats.open_files_count); write_unlock(&global_ft.lock); ksmbd_conn_put(conn); ksmbd_share_config_put(work->tcon->share_conf); - return -EBADF; + goto out; } - ci = fp->f_ci; - 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 || op->o_fp != fp) @@ -2020,7 +1983,6 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) op->sess = work->sess; op->session_id = work->sess->id; } - up_write(&ci->m_lock); spin_lock(&fp->f_lock); fp->owner.uid = fp->owner.gid = 0; @@ -2029,7 +1991,73 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) spin_unlock(&fp->f_lock); ksmbd_share_config_put(old_share); - return 0; + ret = 0; +out: + up_write(&ci->m_lock); + return ret; +} + +/** + * ksmbd_rebind_replay_fd() - bind a completed replay to a surviving channel + * @work: replay request, with a referenced session + * @fp: referenced open whose owner and lease key have been checked + * + * Return: 0 on success, -ENOEXEC for another session, or -EBADF for a closed open. + */ +int ksmbd_rebind_replay_fd(struct ksmbd_work *work, struct ksmbd_file *fp) +{ + struct ksmbd_file_table *ft = &work->sess->file_table; + struct ksmbd_inode *ci = fp->f_ci; + struct ksmbd_conn *old_conn = NULL; + struct oplock_info *op; + int ret = 0; + + /* CLOSE unpublishes under ft->lock. Detach takes m_lock afterward. */ + down_write(&ci->m_lock); + read_lock(&ft->lock); + if (fp->f_state != FP_INITED) { + ret = -EBADF; + goto out; + } + /* Detached durable opens follow the existing reconnect path. */ + if (!fp->conn) + goto out; + if (!has_file_id(fp->volatile_id)) { + ret = -EBADF; + goto out; + } + if (idr_find(ft->idr, fp->volatile_id) != fp) { + ret = -ENOEXEC; + goto out; + } + if (fp->conn == work->conn || + (!ksmbd_conn_exiting(fp->conn) && !ksmbd_conn_releasing(fp->conn))) + goto out; + + write_lock(&global_ft.lock); + old_conn = fp->conn; + fp->conn = ksmbd_conn_get(work->conn); + atomic_inc(&fp->conn->stats.open_files_count); + atomic_dec(&old_conn->stats.open_files_count); + write_unlock(&global_ft.lock); + + list_for_each_entry_rcu(op, &ci->m_op_list, op_entry, + lockdep_is_held(&ci->m_lock)) { + struct ksmbd_conn *conn; + + if (op->o_fp != fp) + continue; + conn = op->conn; + op->conn = ksmbd_conn_get(fp->conn); + op->sess = work->sess; + op->session_id = work->sess->id; + ksmbd_conn_put(conn); + } +out: + read_unlock(&ft->lock); + up_write(&ci->m_lock); + ksmbd_conn_put(old_conn); + return ret; } int ksmbd_init_file_table(struct ksmbd_file_table *ft) diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 6df48ae04548..694453c75ec4 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -260,6 +260,7 @@ void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, 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_rebind_replay_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); int ksmbd_init_file_cache(void); From a15d42f53184f5abda0067b7ebe9edb82d05a0ea Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 25 Sep 2026 08:44:00 +0900 Subject: [PATCH 60/95] ksmbd: track SMB2 locks above OFFSET_MAX in server lock table SMB2 lock ranges use unsigned 64-bit offsets, but file_lock stores signed loff_t values. Track valid ranges above OFFSET_MAX in ksmbd's inode-based lock index and arbitrate conflicts there. Keep these offsets out of file_lock, and handle blocking requests, unlocks, rollback, close cleanup and cancellation in the SMB layer. Reserve large ranges under the index spinlock until the full request commits. Keep lookup and ownership independent of the connection so channel removal and hard-link opens use the same conflict checks. Finish large-range unlocks in the SMB layer after unlocking any VFS portion. Wake large-range lock waiters when normal locks are removed by unlock or close. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 235 +++++++++++++++++++++++++++++++------- fs/smb/server/vfs_cache.c | 119 ++++++++++++++++++- fs/smb/server/vfs_cache.h | 9 ++ 3 files changed, 321 insertions(+), 42 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 77823c3462c5..c4addc1181b6 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10226,8 +10226,10 @@ static int smb2_set_flock_flags(struct file_lock *flock, int flags) return cmd; } +/* Track SMB locks that cannot be represented by a signed loff_t. */ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, unsigned int cmd, int flags, bool zero_len, + u64 start, u64 end, bool virtual_lock, struct list_head *lock_list) { struct ksmbd_lock *lock; @@ -10238,10 +10240,11 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, lock->cmd = cmd; lock->fl = flock; - lock->start = flock->fl_start; - lock->end = flock->fl_end; + lock->start = start; + lock->end = end; lock->flags = flags; lock->zero_len = zero_len; + lock->virtual_lock = virtual_lock; INIT_HLIST_NODE(&lock->hlist); INIT_LIST_HEAD(&lock->flist); INIT_LIST_HEAD(&lock->llist); @@ -10250,6 +10253,106 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, return lock; } +static void smb2_virtual_lock_cancel(void **argv) +{ + (void)argv; + wake_up_all(&ksmbd_lock_wait); +} + +static int smb2_virtual_lock_wait(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct ksmbd_lock *smb_lock, + struct smb2_lock_rsp *rsp) +{ + void **argv = NULL; + bool async = false; + int ret; + + for (;;) { + ret = ksmbd_file_lock_reserve(fp, smb_lock); + if (ret != -EAGAIN || + (smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY)) + break; + + if (!async) { + argv = kmalloc_obj(*argv, KSMBD_DEFAULT_GFP); + if (!argv) + return -ENOMEM; + argv[0] = NULL; + ret = setup_async_work(work, smb2_virtual_lock_cancel, argv); + if (ret) { + kfree(argv); + return ret; + } + async = true; + + read_lock(&work->sess->file_table.lock); + if (fp->f_state != FP_INITED) { + read_unlock(&work->sess->file_table.lock); + release_async_work(work); + return -ENOENT; + } + spin_lock(&fp->f_lock); + list_add(&work->fp_entry, &fp->blocked_works); + spin_unlock(&fp->f_lock); + read_unlock(&work->sess->file_table.lock); + smb2_send_interim_resp(work, STATUS_PENDING); + } + + wait_event(ksmbd_lock_wait, + READ_ONCE(work->state) != KSMBD_WORK_ACTIVE || + !ksmbd_file_lock_conflict(smb_lock)); + + if (READ_ONCE(work->state) != KSMBD_WORK_ACTIVE) { + spin_lock(&fp->f_lock); + list_del_init(&work->fp_entry); + spin_unlock(&fp->f_lock); + if (work->state == KSMBD_WORK_CANCELLED) { + rsp->hdr.Status = STATUS_CANCELLED; + smb2_send_interim_resp(work, STATUS_CANCELLED); + work->send_no_response = 1; + ret = -ECANCELED; + } else { + rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; + ret = -EIO; + } + release_async_work(work); + return ret; + } + } + + if (async) { + bool cancelled; + + spin_lock(&fp->f_lock); + list_del_init(&work->fp_entry); + cancelled = READ_ONCE(work->state) != KSMBD_WORK_ACTIVE; + spin_unlock(&fp->f_lock); + release_async_work(work); + if (cancelled) { + ksmbd_file_lock_remove(smb_lock); + if (work->state == KSMBD_WORK_CANCELLED) { + rsp->hdr.Status = STATUS_CANCELLED; + smb2_send_interim_resp(work, STATUS_CANCELLED); + work->send_no_response = 1; + return -ECANCELED; + } + rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; + return -EIO; + } + } + + return ret; +} + +static void smb2_free_ksmbd_lock(struct ksmbd_lock *smb_lock) +{ + if (smb_lock->vfs_locked) + ksmbd_vfs_posix_lock_unblock(smb_lock->fl); + locks_free_lock(smb_lock->fl); + kfree(smb_lock); +} + static void smb2_remove_blocked_lock(void **argv) { struct file_lock *flock = (struct file_lock *)argv[0]; @@ -10258,12 +10361,6 @@ 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); @@ -10289,13 +10386,14 @@ int smb2_lock(struct ksmbd_work *work) int flags = 0; int cmd = 0; int err = -EIO, i, rc = 0; - u64 lock_start, lock_length; + u64 lock_start, lock_length, lock_end; struct ksmbd_lock *smb_lock = NULL, *cmp_lock, *tmp; LIST_HEAD(lock_list); LIST_HEAD(rollback_list); int prior_lock = 0; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; bool lock_replayed; + bool virtual_lock; WORK_BUFFERS(work, req, rsp); @@ -10358,23 +10456,29 @@ 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 > OFFSET_MAX || - (lock_length && - lock_length - 1 > OFFSET_MAX - lock_start)) { + if (lock_length && lock_length - 1 > U64_MAX - lock_start) { pr_err("Invalid lock range requested\n"); 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; + lock_end = lock_length ? lock_start + lock_length - 1 : lock_start; + virtual_lock = lock_end > OFFSET_MAX; + + /* Lock the representable portion in VFS as well. */ + if (lock_start > OFFSET_MAX) { + flock->fl_start = 0; + flock->fl_end = 0; + } else { + flock->fl_start = lock_start; + flock->fl_end = min_t(u64, lock_end, OFFSET_MAX); + } /* Check conflict locks in one request */ list_for_each_entry(cmp_lock, &lock_list, llist) { - if (cmp_lock->fl->fl_start <= flock->fl_start && - cmp_lock->fl->fl_end >= flock->fl_end) { + if (cmp_lock->start <= lock_start && + cmp_lock->end >= lock_end) { if (cmp_lock->fl->c.flc_type != F_UNLCK && flock->c.flc_type != F_UNLCK) { pr_err("conflict two locks in one request\n"); @@ -10386,6 +10490,7 @@ int smb2_lock(struct ksmbd_work *work) } smb_lock = smb2_lock_init(flock, cmd, flags, !lock_length, + lock_start, lock_end, virtual_lock, &lock_list); if (!smb_lock) { err = -EINVAL; @@ -10422,6 +10527,22 @@ int smb2_lock(struct ksmbd_work *work) prior_lock = smb_lock->flags; + /* The SMB lock table stores ranges that file_lock cannot express. */ + if (smb_lock->virtual_lock && + !(smb_lock->flags & SMB2_LOCKFLAG_UNLOCK)) + goto no_check_cl; + if (smb_lock->virtual_lock && + (smb_lock->flags & SMB2_LOCKFLAG_UNLOCK)) { + err = ksmbd_file_lock_unlock(smb_lock); + if (err) { + rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; + goto out; + } + list_del(&smb_lock->llist); + smb2_free_ksmbd_lock(smb_lock); + continue; + } + if (!(smb_lock->flags & SMB2_LOCKFLAG_UNLOCK) && !(smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY)) goto no_check_cl; @@ -10437,7 +10558,21 @@ int smb2_lock(struct ksmbd_work *work) flock = smb_lock->fl; list_del(&smb_lock->llist); + if (smb_lock->virtual_lock && + !(smb_lock->flags & SMB2_LOCKFLAG_UNLOCK)) { + rc = smb2_virtual_lock_wait(work, fp, smb_lock, rsp); + if (rc) { + err = rc; + smb2_free_ksmbd_lock(smb_lock); + goto out; + } + list_add(&smb_lock->llist, &rollback_list); + if (smb_lock->start > OFFSET_MAX) + continue; + } + if (smb_lock->zero_len) { + rc = 0; err = 0; goto skip; } @@ -10464,6 +10599,11 @@ int smb2_lock(struct ksmbd_work *work) argv = kmalloc(sizeof(void *), KSMBD_DEFAULT_GFP); if (!argv) { err = -ENOMEM; + if (smb_lock->virtual_lock) { + ksmbd_vfs_posix_lock_unblock(flock); + locks_wake_up(flock); + goto out; + } smb2_free_blocked_lock(flock); kfree(smb_lock); goto out; @@ -10476,11 +10616,17 @@ int smb2_lock(struct ksmbd_work *work) if (rc) { kfree(argv); err = -ENOMEM; + if (smb_lock->virtual_lock) { + ksmbd_vfs_posix_lock_unblock(flock); + locks_wake_up(flock); + goto out; + } smb2_free_blocked_lock(flock); kfree(smb_lock); goto out; } - list_add(&smb_lock->llist, &rollback_list); + if (!smb_lock->virtual_lock) + 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); @@ -10493,16 +10639,19 @@ int smb2_lock(struct ksmbd_work *work) list_del(&work->fp_entry); spin_unlock(&fp->f_lock); - list_del(&smb_lock->llist); + if (!smb_lock->virtual_lock) + list_del(&smb_lock->llist); 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; + if (!smb_lock->virtual_lock) { + kfree(smb_lock); + locks_free_lock(flock); + } goto out; } @@ -10511,20 +10660,25 @@ int smb2_lock(struct ksmbd_work *work) if (work->state == KSMBD_WORK_ACTIVE) goto retry; - locks_free_lock(flock); - rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; - kfree(smb_lock); + if (!smb_lock->virtual_lock) { + locks_free_lock(flock); + kfree(smb_lock); + } /* rollback_list may still hold earlier grants */ goto out; } else if (!rc) { - list_add(&smb_lock->llist, &rollback_list); + smb_lock->vfs_locked = !smb_lock->zero_len; + if (!smb_lock->virtual_lock) + list_add(&smb_lock->llist, &rollback_list); ksmbd_debug(SMB, "successful in taking lock\n"); } else { - locks_free_lock(flock); - kfree(smb_lock); err = rc; + if (!smb_lock->virtual_lock) { + locks_free_lock(flock); + kfree(smb_lock); + } goto out; } } @@ -10561,24 +10715,27 @@ int smb2_lock(struct ksmbd_work *work) list_for_each_entry_safe(smb_lock, tmp, &rollback_list, llist) { struct file_lock *rlock = NULL; - rlock = smb_flock_init(filp); - if (rlock) { - rlock->c.flc_type = F_UNLCK; - rlock->fl_start = smb_lock->start; - rlock->fl_end = smb_lock->end; + if (smb_lock->vfs_locked) { + rlock = smb_flock_init(filp); + if (rlock) { + rlock->c.flc_type = F_UNLCK; + rlock->fl_start = smb_lock->fl->fl_start; + rlock->fl_end = smb_lock->fl->fl_end; - rc = vfs_lock_file(filp, F_SETLK, rlock, NULL); - if (rc) - pr_err("rollback unlock fail : %d\n", rc); - } else { - pr_err("rollback unlock alloc failed\n"); + rc = vfs_lock_file(filp, F_SETLK, rlock, NULL); + if (rc) + pr_err("rollback unlock fail : %d\n", rc); + } else { + pr_err("rollback unlock alloc failed\n"); + } } + if (smb_lock->virtual_lock) + ksmbd_file_lock_remove(smb_lock); list_del(&smb_lock->llist); - smb2_free_lock(smb_lock->fl); if (rlock) locks_free_lock(rlock); - kfree(smb_lock); + smb2_free_ksmbd_lock(smb_lock); } out2: ksmbd_debug(SMB, "failed in taking lock(flags : %x), err : %d\n", flags, err); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index c5217433299e..e5dbc8e41bfb 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -38,6 +38,7 @@ static DEFINE_RWLOCK(inode_hash_lock); static struct ksmbd_file_table global_ft; static atomic_long_t fd_limit; static struct kmem_cache *filp_cache; +DECLARE_WAIT_QUEUE_HEAD(ksmbd_lock_wait); /* Both the inode hash and each open's lock_list use this lock. */ static DEFINE_SPINLOCK(file_locks_lock); @@ -618,6 +619,109 @@ static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp write_unlock(&ft->lock); } +/* file_locks_lock protects both committed locks and pending reservations. */ +static bool __ksmbd_file_lock_conflict(struct ksmbd_lock *lock) +{ + struct inode *inode = file_inode(lock->fl->c.flc_file); + struct ksmbd_lock *cmp; + + hash_for_each_possible(file_locks, cmp, hlist, (unsigned long)inode) { + if (cmp == lock || file_inode(cmp->fl->c.flc_file) != inode) + continue; + if ((cmp->flags & SMB2_LOCKFLAG_SHARED) && + (lock->flags & SMB2_LOCKFLAG_SHARED)) + continue; + if ((cmp->zero_len && !lock->zero_len && + cmp->start > lock->start && cmp->start <= lock->end) || + (lock->zero_len && !cmp->zero_len && + lock->start > cmp->start && lock->start <= cmp->end) || + (!cmp->zero_len && !lock->zero_len && + cmp->start <= lock->end && lock->start <= cmp->end)) + return true; + } + return false; +} + +bool ksmbd_file_lock_conflict(struct ksmbd_lock *lock) +{ + bool conflict; + + spin_lock(&file_locks_lock); + conflict = __ksmbd_file_lock_conflict(lock); + spin_unlock(&file_locks_lock); + return conflict; +} + +int ksmbd_file_lock_reserve(struct ksmbd_file *fp, struct ksmbd_lock *lock) +{ + int ret = 0; + + spin_lock(&file_locks_lock); + if (__ksmbd_file_lock_conflict(lock)) { + ret = -EAGAIN; + } else { + /* Reserve the range until the whole LOCK request commits. */ + lock->pending = true; + hash_add(file_locks, &lock->hlist, + (unsigned long)file_inode(fp->filp)); + list_add_tail(&lock->flist, &fp->lock_list); + } + spin_unlock(&file_locks_lock); + return ret; +} + +void ksmbd_file_lock_remove(struct ksmbd_lock *lock) +{ + spin_lock(&file_locks_lock); + if (!hlist_unhashed(&lock->hlist)) { + hash_del(&lock->hlist); + list_del_init(&lock->flist); + } + spin_unlock(&file_locks_lock); + wake_up_all(&ksmbd_lock_wait); +} + +/* Unlock a large range, including any portion represented by a VFS lock. */ +int ksmbd_file_lock_unlock(struct ksmbd_lock *lock) +{ + struct inode *inode = file_inode(lock->fl->c.flc_file); + struct ksmbd_lock *cmp, *match = NULL; + int ret = 0; + + spin_lock(&file_locks_lock); + hash_for_each_possible(file_locks, cmp, hlist, (unsigned long)inode) { + if (cmp->fl->c.flc_file != lock->fl->c.flc_file || + cmp->start != lock->start || cmp->end != lock->end || + cmp->pending) + continue; + if (waitqueue_active(&cmp->fl->c.flc_wait)) /* VFS waiters */ + continue; + match = cmp; + if (cmp->flags & SMB2_LOCKFLAG_EXCLUSIVE) + break; + } + if (match) + match->pending = true; + spin_unlock(&file_locks_lock); + if (!match) + return -ENOENT; + + if (match->vfs_locked) + ret = vfs_lock_file(lock->fl->c.flc_file, F_SETLK, lock->fl, NULL); + if (ret) { + spin_lock(&file_locks_lock); + match->pending = false; + spin_unlock(&file_locks_lock); + return ret; + } + ksmbd_file_lock_remove(match); + if (match->vfs_locked) + ksmbd_vfs_posix_lock_unblock(match->fl); + locks_free_lock(match->fl); + kfree(match); + return 0; +} + /** * ksmbd_file_lock_check() - check conflicts or remove an exact unlock match * @lock: unregistered lock from the current request @@ -637,7 +741,8 @@ int ksmbd_file_lock_check(struct ksmbd_lock *lock) if (lock_is_unlock(lock->fl)) { if (cmp->fl->c.flc_file != lock->fl->c.flc_file || - cmp->start != lock->start || cmp->end != lock->end) + cmp->start != lock->start || cmp->end != lock->end || + cmp->pending) continue; if (waitqueue_active(&cmp->fl->c.flc_wait)) /* VFS waiters */ continue; @@ -673,9 +778,11 @@ int ksmbd_file_lock_check(struct ksmbd_lock *lock) } spin_unlock(&file_locks_lock); if (lock_to_remove) { - ksmbd_vfs_posix_lock_unblock(lock_to_remove->fl); + if (lock_to_remove->vfs_locked) + ksmbd_vfs_posix_lock_unblock(lock_to_remove->fl); locks_free_lock(lock_to_remove->fl); kfree(lock_to_remove); + wake_up_all(&ksmbd_lock_wait); } return ret; } @@ -692,6 +799,10 @@ void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks) spin_lock(&file_locks_lock); list_for_each_entry_safe(lock, tmp, locks, llist) { list_del_init(&lock->llist); + if (!hlist_unhashed(&lock->hlist)) { + lock->pending = false; + continue; + } hash_add(file_locks, &lock->hlist, (unsigned long)file_inode(fp->filp)); list_add_tail(&lock->flist, &fp->lock_list); @@ -712,10 +823,12 @@ static void ksmbd_file_lock_clear(struct ksmbd_file *fp) list_for_each_entry_safe(lock, tmp, &dispose, flist) { list_del_init(&lock->flist); - ksmbd_vfs_posix_lock_unblock(lock->fl); + if (lock->vfs_locked) + ksmbd_vfs_posix_lock_unblock(lock->fl); locks_free_lock(lock->fl); kfree(lock); } + wake_up_all(&ksmbd_lock_wait); } static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 694453c75ec4..6831aca22cff 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "vfs.h" @@ -44,12 +45,20 @@ struct ksmbd_lock { unsigned int flags; int cmd; int zero_len; + bool virtual_lock; + bool vfs_locked; + bool pending; unsigned long long start; unsigned long long end; }; int ksmbd_file_lock_check(struct ksmbd_lock *lock); void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks); +bool ksmbd_file_lock_conflict(struct ksmbd_lock *lock); +int ksmbd_file_lock_reserve(struct ksmbd_file *fp, struct ksmbd_lock *lock); +void ksmbd_file_lock_remove(struct ksmbd_lock *lock); +int ksmbd_file_lock_unlock(struct ksmbd_lock *lock); +extern wait_queue_head_t ksmbd_lock_wait; struct stream { char *name; From defe50d42de812986f613fd33ab2bde0426d7653 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 25 Sep 2026 19:39:16 +0900 Subject: [PATCH 61/95] ksmbd: expire unacknowledged oplock breaks Arm the oplock break acknowledgment timer once the common notification path accepts a classic break for transmission. Preserve its shared retry deadline when another channel is tried. Store a deadline per open and scan the global open table when the earliest deadline expires. Drop expired breaks to NONE and restart the timer while other breaks remain. Serialize timer expiry with client acknowledgments and close using the oplock state lock. Wake blocked requests when a break expires or sending the notification fails, and invalidate durable reconnect for the open. Use wake_up_all() for acknowledgment, timeout, close and send failure. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 30 ++++++++++++++-- fs/smb/server/oplock.h | 3 ++ fs/smb/server/smb2pdu.c | 6 ++-- fs/smb/server/vfs_cache.c | 74 +++++++++++++++++++++++++++++++++++++++ fs/smb/server/vfs_cache.h | 1 + 5 files changed, 110 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 9111cf727dd9..295cfd42e471 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -551,9 +551,10 @@ void close_id_del_oplock(struct ksmbd_file *fp) * takes the same lock before it acquires pending_break or sets ACK_WAIT. */ opinfo->op_state = OPLOCK_CLOSING; + opinfo->oplock_timeout_set = false; clear_bit_unlock(0, &opinfo->pending_break); spin_unlock(&opinfo->state_lock); - wake_up_interruptible_all(&opinfo->oplock_q); + wake_up_all(&opinfo->oplock_q); if (opinfo->is_lease) wake_up_interruptible_all(&opinfo->oplock_brk); /* memory barrier is needed for wake_up_bit() */ @@ -744,6 +745,8 @@ static bool oplock_break_set_ack_wait(struct oplock_info *opinfo) spin_lock(&opinfo->state_lock); if (opinfo->op_state != OPLOCK_CLOSING) { opinfo->op_state = OPLOCK_ACK_WAIT; + opinfo->oplock_timeout_set = false; + opinfo->oplock_timed_out = false; ret = true; } spin_unlock(&opinfo->state_lock); @@ -816,6 +819,23 @@ static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level, return opinfo->level > req_op_level; } +static void smb2_break_start_timer(struct oplock_info *opinfo, + unsigned long timeout) +{ + bool start = false; + + spin_lock(&opinfo->state_lock); + if (!opinfo->is_lease && opinfo->op_state == OPLOCK_ACK_WAIT && + !opinfo->oplock_timeout_set) { + opinfo->oplock_timeout = timeout; + opinfo->oplock_timeout_set = true; + start = true; + } + spin_unlock(&opinfo->state_lock); + if (start) + ksmbd_schedule_oplock_break_timer(timeout); +} + struct smb2_break_channel { struct list_head list; struct ksmbd_conn *conn; @@ -853,8 +873,10 @@ static int smb2_break_failed(struct oplock_info *opinfo, bool expect_ack) } opinfo->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->op_state = OPLOCK_STATE_NONE; + opinfo->oplock_timeout_set = false; + opinfo->oplock_timed_out = true; spin_unlock(&opinfo->state_lock); - wake_up_interruptible_all(&opinfo->oplock_q); + wake_up_all(&opinfo->oplock_q); wake_up_interruptible_all(&opinfo->oplock_brk); return ksmbd_invalidate_durable_fd(opinfo->fid); } @@ -1169,6 +1191,8 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci, continue; } channel->queued = true; + if (expect_ack) + smb2_break_start_timer(opinfo, deadline); if (interim_work) { ksmbd_queue_io_work(interim_work); interim_work = NULL; @@ -1209,6 +1233,8 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci, failed: ret = smb2_break_failed(opinfo, expect_ack); out: + if (expect_ack && READ_ONCE(opinfo->oplock_timed_out)) + ret = ksmbd_invalidate_durable_fd(opinfo->fid); ksmbd_free_work_struct(work); list_for_each_entry_safe(channel, tmp, &channels, list) { list_del(&channel->list); diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 5ef05166e502..03a5b93da2ec 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -67,6 +67,9 @@ struct oplock_info { int level; int op_state; spinlock_t state_lock; + unsigned long oplock_timeout; + bool oplock_timeout_set; + bool oplock_timed_out; 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 c4addc1181b6..ba266e347989 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -12030,8 +12030,9 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) opinfo->level = rsp_oplevel; opinfo->op_state = OPLOCK_STATE_NONE; + opinfo->oplock_timeout_set = false; spin_unlock(&opinfo->state_lock); - wake_up_interruptible_all(&opinfo->oplock_q); + wake_up_all(&opinfo->oplock_q); rsp->StructureSize = cpu_to_le16(24); rsp->OplockLevel = rsp_oplevel; @@ -12048,8 +12049,9 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) err_out: if (opinfo->op_state != OPLOCK_CLOSING) opinfo->op_state = OPLOCK_STATE_NONE; + opinfo->oplock_timeout_set = false; spin_unlock(&opinfo->state_lock); - wake_up_interruptible_all(&opinfo->oplock_q); + wake_up_all(&opinfo->oplock_q); rsp->hdr.Status = status; smb2_set_err_rsp(work); out_no_state_change: diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index e5dbc8e41bfb..d63fed289c79 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -12,6 +12,7 @@ #include #include #include +#include #include "glob.h" #include "vfs_cache.h" @@ -36,6 +37,75 @@ static struct hlist_head *inode_hashtable __read_mostly; static DEFINE_RWLOCK(inode_hash_lock); static struct ksmbd_file_table global_ft; +static DEFINE_MUTEX(oplock_break_timer_lock); +static struct delayed_work oplock_break_timer_work; +static unsigned long oplock_break_timer_next; +static bool oplock_break_timer_pending; + +static void ksmbd_oplock_break_timer(struct work_struct *work) +{ + struct ksmbd_file *fp; + struct oplock_info *opinfo; + unsigned long next = 0, now; + unsigned int id; + bool pending = false; + bool expired; + + mutex_lock(&oplock_break_timer_lock); + now = jiffies; + read_lock(&global_ft.lock); + idr_for_each_entry(global_ft.idr, fp, id) { + opinfo = opinfo_get(fp); + if (!opinfo) + continue; + + expired = false; + spin_lock(&opinfo->state_lock); + if (!opinfo->is_lease && + opinfo->op_state == OPLOCK_ACK_WAIT && + opinfo->oplock_timeout_set) { + if (time_after_eq(now, opinfo->oplock_timeout)) { + /* ksmbd owns the oplock state for the local file. */ + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + opinfo->op_state = OPLOCK_STATE_NONE; + opinfo->oplock_timeout_set = false; + opinfo->oplock_timed_out = true; + expired = true; + } else if (!pending || + time_before(opinfo->oplock_timeout, next)) { + next = opinfo->oplock_timeout; + pending = true; + } + } + spin_unlock(&opinfo->state_lock); + if (expired) + wake_up_all(&opinfo->oplock_q); + opinfo_put(opinfo); + } + read_unlock(&global_ft.lock); + + oplock_break_timer_pending = pending; + if (pending) { + oplock_break_timer_next = next; + mod_delayed_work(system_wq, &oplock_break_timer_work, + time_after_eq(jiffies, next) ? 0 : next - jiffies); + } + mutex_unlock(&oplock_break_timer_lock); +} + +void ksmbd_schedule_oplock_break_timer(unsigned long timeout) +{ + mutex_lock(&oplock_break_timer_lock); + if (!oplock_break_timer_pending || + time_before(timeout, oplock_break_timer_next)) { + oplock_break_timer_pending = true; + oplock_break_timer_next = timeout; + mod_delayed_work(system_wq, &oplock_break_timer_work, + time_after_eq(jiffies, timeout) ? 0 : timeout - jiffies); + } + mutex_unlock(&oplock_break_timer_lock); +} + static atomic_long_t fd_limit; static struct kmem_cache *filp_cache; DECLARE_WAIT_QUEUE_HEAD(ksmbd_lock_wait); @@ -1965,6 +2035,8 @@ void ksmbd_close_session_fds(struct ksmbd_work *work) int ksmbd_init_global_file_table(void) { + INIT_DELAYED_WORK(&oplock_break_timer_work, ksmbd_oplock_break_timer); + oplock_break_timer_pending = false; if (create_proc_files()) pr_warn("Unable to create files procfs entry\n"); return ksmbd_init_file_table(&global_ft); @@ -1975,6 +2047,8 @@ void ksmbd_free_global_file_table(void) struct ksmbd_file *fp = NULL; unsigned int id; + cancel_delayed_work_sync(&oplock_break_timer_work); + idr_for_each_entry(global_ft.idr, fp, id) { ksmbd_remove_durable_fd(fp); __ksmbd_close_fd(NULL, fp); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 6831aca22cff..9de41b6e28ba 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -242,6 +242,7 @@ void ksmbd_wake_session_blocked_works(struct ksmbd_session *sess); int ksmbd_close_inode_fds(struct ksmbd_work *work, struct inode *inode); int ksmbd_init_global_file_table(void); void ksmbd_free_global_file_table(void); +void ksmbd_schedule_oplock_break_timer(unsigned long timeout); void ksmbd_set_fd_limit(unsigned long limit); int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp, unsigned int state); From c1c50e2dbbd55fb141da4146214b6fc301bf89dd Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 25 Sep 2026 19:53:01 +0900 Subject: [PATCH 62/95] ksmbd: expire unacknowledged lease breaks Arm a lease break acknowledgment timer once the common notification path accepts an ACK-required notification for transmission. Keep the deadline on the lease across channel retries and scan every lease in the global lease tables when the timer expires. Set timed-out leases and all their opens to NONE, then rearm for the next deadline. Serialize client acknowledgments, notification state and timer expiry under the lease lock, taking each open's state lock after it. Update every open before publishing break completion so a late acknowledgment cannot restore an expired lease. Wake blocked opens on timeout, send failure and global lease-table teardown, and cancel the timer at teardown. Keep the existing connection-independent lease-table lifetime and use the notifier's opinfo reference to retain the lease. Stop after selecting one open during lease-key lookup. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 188 +++++++++++++++++++++++++++++++++++----- fs/smb/server/oplock.h | 3 + fs/smb/server/smb2pdu.c | 30 +++++-- 3 files changed, 193 insertions(+), 28 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 295cfd42e471..bb2d4bdf51db 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -20,6 +20,11 @@ static LIST_HEAD(lease_table_list); static DEFINE_RWLOCK(lease_list_lock); +static DEFINE_MUTEX(lease_break_timer_lock); +static unsigned long lease_break_timer_next; +static bool lease_break_timer_pending; +static void lease_break_timer(struct work_struct *work); +static DECLARE_DELAYED_WORK(lease_break_timer_work, lease_break_timer); #define SMB2_LEASE_STATE_MASK_LE (SMB2_LEASE_READ_CACHING_LE | \ SMB2_LEASE_HANDLE_CACHING_LE | \ @@ -116,6 +121,82 @@ static void lease_put(struct lease *lease) kfree(lease); } +/* The caller holds lease->lock. */ +static void lease_break_to_none_locked(struct lease *lease) +{ + struct oplock_info *opinfo; + bool wake; + + lease->state = SMB2_LEASE_NONE_LE; + lease->new_state = SMB2_LEASE_NONE_LE; + lease->breaking = false; + lease->break_timeout_set = false; + list_for_each_entry(opinfo, &lease->open_list, lease_entry) { + spin_lock(&opinfo->state_lock); + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + wake = opinfo->op_state == OPLOCK_ACK_WAIT; + if (wake) { + opinfo->op_state = OPLOCK_STATE_NONE; + opinfo->oplock_timed_out = true; + atomic_set(&opinfo->breaking_cnt, 0); + } + spin_unlock(&opinfo->state_lock); + if (wake) { + wake_up_all(&opinfo->oplock_q); + wake_up_interruptible_all(&opinfo->oplock_brk); + } + } +} + +static void lease_break_timer(struct work_struct *work) +{ + struct lease_table *lb; + struct lease *lease; + unsigned long next = 0, now; + bool pending = false; + + mutex_lock(&lease_break_timer_lock); + now = jiffies; + read_lock(&lease_list_lock); + list_for_each_entry(lb, &lease_table_list, l_entry) { + list_for_each_entry(lease, &lb->lease_list, l_entry) { + spin_lock(&lease->lock); + if (lease->breaking && lease->break_timeout_set) { + if (time_after_eq(now, lease->break_timeout)) { + lease_break_to_none_locked(lease); + } else if (!pending || + time_before(lease->break_timeout, next)) { + next = lease->break_timeout; + pending = true; + } + } + spin_unlock(&lease->lock); + } + } + read_unlock(&lease_list_lock); + + lease_break_timer_pending = pending; + if (pending) { + lease_break_timer_next = next; + mod_delayed_work(system_wq, &lease_break_timer_work, + time_after_eq(jiffies, next) ? 0 : next - jiffies); + } + mutex_unlock(&lease_break_timer_lock); +} + +static void schedule_lease_break_timer(unsigned long timeout) +{ + mutex_lock(&lease_break_timer_lock); + if (!lease_break_timer_pending || + time_before(timeout, lease_break_timer_next)) { + lease_break_timer_pending = true; + lease_break_timer_next = timeout; + mod_delayed_work(system_wq, &lease_break_timer_work, + time_after_eq(jiffies, timeout) ? 0 : timeout - jiffies); + } + mutex_unlock(&lease_break_timer_lock); +} + static void lease_add_table(struct lease *lease, struct lease_table *lb) { lease_get(lease); @@ -190,6 +271,8 @@ static struct lease *alloc_lease(struct lease_ctx_info *lctx, lease->epoch = lctx->version == 2 ? le16_to_cpu(lctx->epoch) + 1 : 0; lease->ci = ci; lease->reuse_epoch = false; + lease->breaking = false; + lease->break_timeout_set = false; lease->l_lb = NULL; INIT_LIST_HEAD(&lease->l_entry); INIT_LIST_HEAD(&lease->open_list); @@ -822,11 +905,38 @@ static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level, static void smb2_break_start_timer(struct oplock_info *opinfo, unsigned long timeout) { - bool start = false; + struct lease *lease = opinfo->o_lease; + struct oplock_info *open; + bool start = false, waiting; + + if (opinfo->is_lease) { + read_lock(&lease_list_lock); + spin_lock(&lease->lock); + if (!lease->l_lb) { + lease_break_to_none_locked(lease); + } else if (!lease->breaking) { + list_for_each_entry(open, &lease->open_list, lease_entry) { + spin_lock(&open->state_lock); + waiting = open->op_state == OPLOCK_ACK_WAIT; + spin_unlock(&open->state_lock); + if (!waiting) + continue; + lease->break_timeout = timeout; + lease->break_timeout_set = true; + lease->breaking = true; + start = true; + break; + } + } + spin_unlock(&lease->lock); + read_unlock(&lease_list_lock); + if (start) + schedule_lease_break_timer(timeout); + return; + } spin_lock(&opinfo->state_lock); - if (!opinfo->is_lease && opinfo->op_state == OPLOCK_ACK_WAIT && - !opinfo->oplock_timeout_set) { + if (opinfo->op_state == OPLOCK_ACK_WAIT && !opinfo->oplock_timeout_set) { opinfo->oplock_timeout = timeout; opinfo->oplock_timeout_set = true; start = true; @@ -858,25 +968,33 @@ static bool smb2_break_complete(struct oplock_info *opinfo, bool expect_ack) static int smb2_break_failed(struct oplock_info *opinfo, bool expect_ack) { + struct lease *lease = opinfo->o_lease; + bool timed_out; + + if (opinfo->is_lease) + spin_lock(&lease->lock); spin_lock(&opinfo->state_lock); /* An ACK or close which won the race has already completed the break. */ if (opinfo->op_state == OPLOCK_CLOSING || (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) { + timed_out = opinfo->oplock_timed_out; spin_unlock(&opinfo->state_lock); - return 0; + if (opinfo->is_lease) + spin_unlock(&lease->lock); + return timed_out ? ksmbd_invalidate_durable_fd(opinfo->fid) : 0; } if (opinfo->is_lease) { - opinfo->o_lease->state = SMB2_LEASE_NONE_LE; - opinfo->o_lease->new_state = SMB2_LEASE_NONE_LE; - lease_update_oplock_levels(opinfo->o_lease); - atomic_set(&opinfo->breaking_cnt, 0); + spin_unlock(&opinfo->state_lock); + lease_break_to_none_locked(lease); + spin_unlock(&lease->lock); + } else { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + opinfo->op_state = OPLOCK_STATE_NONE; + opinfo->oplock_timeout_set = false; + opinfo->oplock_timed_out = true; + spin_unlock(&opinfo->state_lock); + wake_up_all(&opinfo->oplock_q); } - opinfo->level = SMB2_OPLOCK_LEVEL_NONE; - opinfo->op_state = OPLOCK_STATE_NONE; - opinfo->oplock_timeout_set = false; - opinfo->oplock_timed_out = true; - spin_unlock(&opinfo->state_lock); - wake_up_all(&opinfo->oplock_q); wake_up_interruptible_all(&opinfo->oplock_brk); return ksmbd_invalidate_durable_fd(opinfo->fid); } @@ -1096,6 +1214,7 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci, struct smb2_oplock_break *oplock_rsp; struct smb2_lease_break *lease_rsp; struct lease *lease = opinfo->o_lease; + struct oplock_info *open; unsigned long deadline, retry_at; int ret, size; LIST_HEAD(channels); @@ -1122,10 +1241,14 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci, if (ret) goto failed; + if (opinfo->is_lease) + spin_lock(&lease->lock); spin_lock(&opinfo->state_lock); if (opinfo->op_state == OPLOCK_CLOSING || (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) { spin_unlock(&opinfo->state_lock); + if (opinfo->is_lease) + spin_unlock(&lease->lock); ret = 0; goto out; } @@ -1159,6 +1282,8 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci, size = sizeof(*oplock_rsp); } spin_unlock(&opinfo->state_lock); + if (opinfo->is_lease) + spin_unlock(&lease->lock); ret = ksmbd_iov_pin_rsp(work, oplock_rsp, size); if (ret) goto failed; @@ -1222,12 +1347,16 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci, if (ret) goto failed; if (!expect_ack && opinfo->is_lease) { - spin_lock(&opinfo->state_lock); - if (opinfo->op_state != OPLOCK_CLOSING) { + spin_lock(&lease->lock); + if (READ_ONCE(opinfo->op_state) != OPLOCK_CLOSING) { lease->state = lease->new_state; - lease_update_oplock_levels(lease); + list_for_each_entry(open, &lease->open_list, lease_entry) { + spin_lock(&open->state_lock); + open->level = smb2_map_lease_to_oplock(lease->state); + spin_unlock(&open->state_lock); + } } - spin_unlock(&opinfo->state_lock); + spin_unlock(&lease->lock); } goto out; failed: @@ -1379,6 +1508,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, open_trunc = brk_opinfo->open_trunc; again: + spin_lock(&lease->lock); atomic_inc(&brk_opinfo->breaking_cnt); if (open_trunc) { /* @@ -1413,6 +1543,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, SMB2_LEASE_HANDLE_CACHING_LE); if (expect_ack) { if (!oplock_break_set_ack_wait(brk_opinfo)) { + spin_unlock(&lease->lock); atomic_dec_if_positive(&brk_opinfo->breaking_cnt); clear_and_wake_up_bit(0, &brk_opinfo->pending_break); return -ENOENT; @@ -1427,6 +1558,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci, inc_epoch = false; lease->reuse_epoch = false; } + spin_unlock(&lease->lock); err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack, wait_ack, inc_epoch, sent_interim ? NULL : in_work, @@ -1524,11 +1656,20 @@ void destroy_lease_table(void) struct lease_table *lb; struct lease *lease; + cancel_delayed_work_sync(&lease_break_timer_work); + mutex_lock(&lease_break_timer_lock); + lease_break_timer_pending = false; + mutex_unlock(&lease_break_timer_lock); + write_lock(&lease_list_lock); while (!list_empty(&lease_table_list)) { lb = list_first_entry(&lease_table_list, struct lease_table, l_entry); lease = list_first_entry(&lb->lease_list, struct lease, l_entry); + spin_lock(&lease->lock); + if (lease->breaking) + lease_break_to_none_locked(lease); + spin_unlock(&lease->lock); lease_del_table(lease); } write_unlock(&lease_list_lock); @@ -2551,11 +2692,13 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, list_for_each_entry(lease, <->lease_list, l_entry) { if (memcmp(lease->lease_key, lease_key, SMB2_LEASE_KEY_SIZE)) continue; - if (!(lease->state & (SMB2_LEASE_HANDLE_CACHING_LE | - SMB2_LEASE_WRITE_CACHING_LE))) - break; - spin_lock(&lease->lock); + if (!(lease->state & (SMB2_LEASE_HANDLE_CACHING_LE | + SMB2_LEASE_WRITE_CACHING_LE))) { + spin_unlock(&lease->lock); + break; + } + list_for_each_entry(opinfo, &lease->open_list, lease_entry) { if (!opinfo->op_state || opinfo->op_state == OPLOCK_CLOSING) @@ -2563,6 +2706,7 @@ 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 03a5b93da2ec..d81013c514e3 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -50,6 +50,9 @@ struct lease { unsigned short epoch; bool is_dir; bool reuse_epoch; + bool breaking; + bool break_timeout_set; + unsigned long break_timeout; struct ksmbd_inode *ci; struct lease_table *l_lb; struct list_head l_entry; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ba266e347989..5d9210e3e3ab 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -12086,10 +12086,12 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) struct ksmbd_conn *conn = work->conn; struct smb2_lease_ack *req; struct smb2_lease_ack *rsp; - struct oplock_info *opinfo; + struct oplock_info *opinfo, *open; int ret; __le32 lease_state; struct lease *lease; + u8 level; + bool wake; WORK_BUFFERS(work, req, rsp); @@ -12105,6 +12107,7 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) lease = opinfo->o_lease; /* Publish NONE only after the lease state and all open levels are updated. */ + spin_lock(&lease->lock); spin_lock(&opinfo->state_lock); if (opinfo->op_state != OPLOCK_ACK_WAIT) { if (opinfo->op_state == OPLOCK_STATE_NONE && @@ -12132,17 +12135,31 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) lease_state = req->LeaseState; lease->state = lease_state; lease->new_state = SMB2_LEASE_NONE_LE; - lease_update_oplock_levels(lease); - opinfo->op_state = OPLOCK_STATE_NONE; - atomic_dec_if_positive(&opinfo->breaking_cnt); + lease->breaking = false; + lease->break_timeout_set = false; + level = smb2_map_lease_to_oplock(lease_state); spin_unlock(&opinfo->state_lock); - wake_up_interruptible_all(&opinfo->oplock_q); - wake_up_interruptible_all(&opinfo->oplock_brk); + list_for_each_entry(open, &lease->open_list, lease_entry) { + spin_lock(&open->state_lock); + open->level = level; + wake = open->op_state == OPLOCK_ACK_WAIT; + if (wake) { + open->op_state = OPLOCK_STATE_NONE; + atomic_set(&open->breaking_cnt, 0); + } + spin_unlock(&open->state_lock); + if (wake) { + wake_up_all(&open->oplock_q); + wake_up_interruptible_all(&open->oplock_brk); + } + } + spin_unlock(&lease->lock); goto response; replay_rsp: lease_state = lease->state; spin_unlock(&opinfo->state_lock); + spin_unlock(&lease->lock); response: rsp->StructureSize = cpu_to_le16(36); rsp->Reserved = 0; @@ -12158,6 +12175,7 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) err_unlock: spin_unlock(&opinfo->state_lock); + spin_unlock(&lease->lock); err_out: smb2_set_err_rsp(work); opinfo_put(opinfo); From 33bcc30505fb8acbff2e1fd969df668ed8ce66ee Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:06 +0900 Subject: [PATCH 63/95] ksmbd: validate POSIX negotiate contexts The POSIX negotiate context currently enables extensions without checking its payload, and duplicate contexts are accepted. Reject duplicate contexts and invalid payload lengths. Enable POSIX extensions only for the published tag, and use the same tag constant when building the negotiate response. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 5d9210e3e3ab..ea010e53eff4 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1430,23 +1430,7 @@ static void build_posix_ctxt(struct smb2_posix_neg_context *pneg_ctxt) { pneg_ctxt->ContextType = SMB2_POSIX_EXTENSIONS_AVAILABLE; pneg_ctxt->DataLength = cpu_to_le16(POSIX_CTXT_DATA_LEN); - /* SMB2_CREATE_TAG_POSIX is "0x93AD25509CB411E7B42383DE968BCD7C" */ - pneg_ctxt->Name[0] = 0x93; - pneg_ctxt->Name[1] = 0xAD; - pneg_ctxt->Name[2] = 0x25; - pneg_ctxt->Name[3] = 0x50; - pneg_ctxt->Name[4] = 0x9C; - pneg_ctxt->Name[5] = 0xB4; - pneg_ctxt->Name[6] = 0x11; - pneg_ctxt->Name[7] = 0xE7; - pneg_ctxt->Name[8] = 0xB4; - pneg_ctxt->Name[9] = 0x23; - pneg_ctxt->Name[10] = 0x83; - pneg_ctxt->Name[11] = 0xDE; - pneg_ctxt->Name[12] = 0x96; - pneg_ctxt->Name[13] = 0x8B; - pneg_ctxt->Name[14] = 0xCD; - pneg_ctxt->Name[15] = 0x7C; + memcpy(pneg_ctxt->Name, SMB2_CREATE_TAG_POSIX, POSIX_CTXT_DATA_LEN); } static unsigned int assemble_neg_contexts(struct ksmbd_conn *conn, @@ -1752,6 +1736,7 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, unsigned int neg_ctxt_cnt = le16_to_cpu(req->NegotiateContextCount); __le32 status = STATUS_INVALID_PARAMETER; int compress_ctxt_cnt = 0, rdma_transform_ctxt_cnt = 0; + bool posix_ctxt_seen = false; ksmbd_debug(SMB, "decoding %d negotiate contexts\n", neg_ctxt_cnt); if (len_of_smb <= offset) { @@ -1824,7 +1809,14 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, } else if (pctx->ContextType == SMB2_POSIX_EXTENSIONS_AVAILABLE) { ksmbd_debug(SMB, "deassemble SMB2_POSIX_EXTENSIONS_AVAILABLE context\n"); - conn->posix_ext_supported = true; + if (posix_ctxt_seen || clen != POSIX_CTXT_DATA_LEN) { + status = STATUS_INVALID_PARAMETER; + break; + } + posix_ctxt_seen = true; + if (!memcmp((char *)pctx + sizeof(*pctx), + SMB2_CREATE_TAG_POSIX, POSIX_CTXT_DATA_LEN)) + conn->posix_ext_supported = true; } else if (pctx->ContextType == SMB2_SIGNING_CAPABILITIES) { ksmbd_debug(SMB, "deassemble SMB2_SIGNING_CAPABILITIES context\n"); From 47fadce958a6fa586d6816a04239dd0c4d52369b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:06 +0900 Subject: [PATCH 64/95] ksmbd: validate POSIX create contexts POSIX create contexts are only examined when the tree has POSIX extensions enabled, and IPC requests bypass this validation. Examine POSIX contexts before selecting the IPC create path. Reject duplicate contexts and require an SMB3.1.1 connection, a POSIX-enabled tree and a non-IPC share for POSIX creates. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 14 +++++++++++--- fs/smb/server/smb2pdu.c | 24 ++++++++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index bb2d4bdf51db..234be869dd79 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -2371,6 +2371,9 @@ struct create_context *smb2_find_context_vals(void *open_req, const char *tag, i struct create_context *cc; unsigned int next = 0; char *name; + struct create_context *found = NULL; + bool posix_tag = tag_len == POSIX_CTXT_DATA_LEN && + !memcmp(tag, SMB2_CREATE_TAG_POSIX, POSIX_CTXT_DATA_LEN); struct smb2_create_req *req = (struct smb2_create_req *)open_req; unsigned int remain_len, name_off, name_len, value_off, value_len, cc_len; @@ -2408,13 +2411,18 @@ struct create_context *smb2_find_context_vals(void *open_req, const char *tag, i return ERR_PTR(-EINVAL); name = (char *)cc + name_off; - if (name_len == tag_len && !memcmp(name, tag, name_len)) - return cc; + if (name_len == tag_len && !memcmp(name, tag, name_len)) { + if (!posix_tag) + return cc; + if (found) + return ERR_PTR(-EINVAL); + found = cc; + } remain_len -= next; } while (next != 0); - return NULL; + return found; } /** diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ea010e53eff4..2d250875b0ad 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4263,12 +4263,7 @@ int smb2_open(struct ksmbd_work *work) return -EINVAL; } - if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) { - ksmbd_debug(SMB, "IPC pipe create request\n"); - return create_smb2_pipe(work); - } - - if (req->CreateContextsOffset && tcon->posix_extensions) { + if (req->CreateContextsOffset) { context = smb2_find_context_vals(req, SMB2_CREATE_TAG_POSIX, 16); if (IS_ERR(context)) { rc = PTR_ERR(context); @@ -4276,6 +4271,19 @@ int smb2_open(struct ksmbd_work *work) } else if (context) { struct create_posix *posix = (struct create_posix *)context; + if (conn->dialect != SMB311_PROT_ID) { + rsp->hdr.Status = STATUS_INVALID_INFO_CLASS; + rc = -ENOTTY; + goto err_out2; + } + if (!tcon->posix_extensions) { + rc = -EOPNOTSUPP; + goto err_out2; + } + if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) { + rc = -EOPNOTSUPP; + goto err_out2; + } if (le16_to_cpu(context->DataOffset) + le32_to_cpu(context->DataLength) < sizeof(struct create_posix) - 4) { @@ -4288,6 +4296,10 @@ int smb2_open(struct ksmbd_work *work) posix_ctxt = true; } } + if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) { + ksmbd_debug(SMB, "IPC pipe create request\n"); + return create_smb2_pipe(work); + } if (req->NameLength) { name = smb2_get_name((char *)req + le16_to_cpu(req->NameOffset), From 1b77c97c74bebb030143cf8485aad0f442dfeebd Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:12 +0900 Subject: [PATCH 65/95] ksmbd: enforce POSIX append-only writes POSIX append-only opens do not set O_APPEND, and the SMB append sentinel is rejected as a negative write offset. Track append-only POSIX opens, set O_APPEND and require the U64_MAX offset sentinel for their writes. Accept FILE_WRITE_DATA or FILE_APPEND_DATA for WRITE requests, preserving ordinary SMB write permissions. Use the POSIX append flag and offset check to prevent POSIX append-only opens from overwriting existing data. For synchronous writes, derive the fsync range from the completed write position so append writes flush the range actually written. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 14 ++++++++++++-- fs/smb/server/vfs.c | 3 +-- fs/smb/server/vfs_cache.h | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 2d250875b0ad..b3d24829817d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4735,6 +4735,9 @@ int smb2_open(struct ksmbd_work *work) &may_flags, req->CreateOptions, file_present ? d_inode(path.dentry)->i_mode : 0); + if (posix_ctxt && (req->DesiredAccess & FILE_APPEND_DATA_LE) && + !(req->DesiredAccess & FILE_WRITE_DATA_LE)) + open_flags |= O_APPEND; if (!test_tree_conn_flag(tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { if (open_flags & (O_CREAT | O_TRUNC)) { @@ -4974,6 +4977,7 @@ int smb2_open(struct ksmbd_work *work) FILE_WRITE_ATTRIBUTES_LE | FILE_SYNCHRONIZE_LE)); fp->is_posix_ctxt = posix_ctxt; + fp->posix_append = posix_ctxt && !!(open_flags & O_APPEND); /* fp should be searchable through ksmbd_inode.m_fp_list * after daccess, saccess, attrib_only, and stream are @@ -9800,6 +9804,7 @@ int smb2_write(struct ksmbd_work *work) ssize_t nbytes; char *data_buf; bool writethrough = false, is_rdma_channel = false; + bool append_write; bool async_interim = false; bool chseq_err = false; int err = 0; @@ -9831,7 +9836,8 @@ int smb2_write(struct ksmbd_work *work) return smb2_write_pipe(work); } - offset = le64_to_cpu(req->Offset); + append_write = le64_to_cpu(req->Offset) == U64_MAX; + offset = append_write ? 0 : le64_to_cpu(req->Offset); if (offset < 0) { err = -EINVAL; goto out; @@ -9934,11 +9940,15 @@ int smb2_write(struct ksmbd_work *work) goto out; } - if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_READ_ATTRIBUTES_LE))) { + if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_APPEND_DATA_LE))) { pr_err("Not permitted to write : 0x%x\n", fp->daccess); err = -EACCES; goto out; } + if (append_write != fp->posix_append) { + err = -EINVAL; + goto out; + } if (work->next_smb2_rcv_hdr_off && !req->hdr.NextCommand) { err = setup_async_work(work, NULL, NULL); diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index d21ed90454f9..d4d669f12283 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -526,7 +526,6 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp, ssize_t *written) { struct file *filp; - loff_t offset = *pos; int err = 0; if (work->conn->connection_type) { @@ -573,7 +572,7 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp, *written = err; err = 0; if (sync) { - err = vfs_fsync_range(filp, offset, offset + *written, 0); + err = vfs_fsync_range(filp, *pos - *written, *pos, 0); if (err < 0) pr_err("fsync failed for filename = %pD, err = %d\n", fp->filp, err); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 9de41b6e28ba..c00d92fdf94a 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -171,6 +171,7 @@ struct ksmbd_file { bool durable_replay_consumed; bool is_posix_ctxt; + bool posix_append; struct durable_owner owner; __le16 channel_sequence; unsigned int outstanding_requests; From bfefe73543197d06e43c5669148b5bcc44f2dcff Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:12 +0900 Subject: [PATCH 66/95] ksmbd: apply byte-range lock checks per open Using the tree's POSIX extension flag to skip byte-range lock checks also skips them for ordinary opens on a POSIX-enabled tree. Base the read and write lock checks on each file's POSIX create context instead. Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index d4d669f12283..a1d107e77b34 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -362,7 +362,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count, if (ksmbd_stream_fd(fp)) return ksmbd_vfs_stream_read(fp, rbuf, pos, count); - if (!work->tcon->posix_extensions) { + if (!fp->is_posix_ctxt) { int ret; ret = check_lock_range(filp, *pos, *pos + count - 1, READ); @@ -547,7 +547,7 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp, goto out; } - if (!work->tcon->posix_extensions) { + if (!fp->is_posix_ctxt) { err = check_lock_range(filp, *pos, *pos + count - 1, WRITE); if (err) { pr_err("unable to write due to lock\n"); From 2cddef9fab171a76ab14dce7a05e95a9735213d6 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:18 +0900 Subject: [PATCH 67/95] ksmbd: use requested mode in POSIX creates The share mode helpers treat a requested POSIX mode of zero as an unspecified mode, replacing it with the share's default permissions. Use the requested mode, including zero, before applying the share masks and forced modes. Pass an explicit default-mode sentinel for non-POSIX creates to keep their existing mode selection. Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/share_config.h | 4 ++-- fs/smb/server/smb2pdu.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/mgmt/share_config.h b/fs/smb/server/mgmt/share_config.h index 91691b525907..390c966726f4 100644 --- a/fs/smb/server/mgmt/share_config.h +++ b/fs/smb/server/mgmt/share_config.h @@ -45,7 +45,7 @@ static inline umode_t share_config_create_mode(struct ksmbd_share_config *share, umode_t posix_mode) { - umode_t mode = (posix_mode ?: (umode_t)-1) & share->create_mask; + umode_t mode = posix_mode & share->create_mask; return mode | share->force_create_mode; } @@ -54,7 +54,7 @@ static inline umode_t share_config_directory_mode(struct ksmbd_share_config *share, umode_t posix_mode) { - umode_t mode = (posix_mode ?: (umode_t)-1) & share->directory_mask; + umode_t mode = posix_mode & share->directory_mask; return mode | share->force_directory_mode; } diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index b3d24829817d..79c122689ff8 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4751,7 +4751,7 @@ int smb2_open(struct ksmbd_work *work) /*create file if not present */ if (!file_present) { rc = smb2_creat(work, &path, name, open_flags, - posix_mode, + posix_ctxt ? posix_mode : (umode_t)-1, req->CreateOptions & FILE_DIRECTORY_FILE_LE); if (rc) { if (rc == -ENOENT) { From cb19d0ef45136baab3db5514f17d088a4f24f763 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:18 +0900 Subject: [PATCH 68/95] ksmbd: apply POSIX mode ACEs in security updates Explicit modes carried by NFS mode ACEs can be overwritten by the DACL permission fallback, and security updates discard special mode bits. Track modes supplied by ACCESS_ALLOWED NFS mode ACEs and bypass the permission fallback for them. Apply both permission and special bits when setting an explicit POSIX mode. For SET_INFO, require a POSIX open for explicit mode changes and apply the mode only when DACL security information is requested. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 7 +++++-- fs/smb/server/smbacl.c | 26 +++++++++++++++++++++++--- fs/smb/server/smbacl.h | 4 +++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 79c122689ff8..4be83ae53af2 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3666,7 +3666,8 @@ static int smb2_create_sd_buffer(struct ksmbd_work *work, sizeof(struct create_sd_buf_req)) return -EINVAL; return set_info_sec(work->conn, work->tcon, path, &sd_buf->ntsd, - le32_to_cpu(sd_buf->ccontext.DataLength), true, false); + le32_to_cpu(sd_buf->ccontext.DataLength), true, false, + true, true); } static int ksmbd_acls_fattr(struct smb_fattr *fattr, @@ -8901,7 +8902,9 @@ static int smb2_set_info_sec(struct ksmbd_file *fp, int addition_info, return -EACCES; return set_info_sec(fp->conn, fp->tcon, &fp->filp->f_path, pntsd, - buf_len, false, true); + buf_len, false, true, + fp->conn->dialect == SMB311_PROT_ID && fp->is_posix_ctxt, + addition_info & DACL_SECINFO); } /** diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index f3b1efd8e85a..c959699ace56 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -477,10 +477,12 @@ static int parse_dacl(const struct mnt_idmap *idmap, ppace[i]->access_req = smb_map_generic_desired_access(ppace[i]->access_req); - if (ppace[i]->sid.num_subauth >= 3 && + if (ppace[i]->type == ACCESS_ALLOWED && + ppace[i]->sid.num_subauth >= 3 && !(compare_sids(&ppace[i]->sid, &sid_unix_NFS_mode))) { fattr->cf_mode = le32_to_cpu(ppace[i]->sid.sub_auth[2]); + fattr->cf_posix_mode = true; break; } else if (!compare_sids(&ppace[i]->sid, pownersid)) { acl_mode = access_flags_to_mode(fattr, @@ -546,6 +548,10 @@ static int parse_dacl(const struct mnt_idmap *idmap, ((acl_mode & 0700) >> 6) | 0004; } } + if (fattr->cf_posix_mode) { + ret = 0; + goto out; + } if (owner_found) { /* The owner must be set to at least read-only. */ @@ -1675,7 +1681,8 @@ EXPORT_SYMBOL_IF_KUNIT(smb_check_perm_dacl); 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) + bool type_check, bool get_write, bool posix_open, + bool dacl_requested) { int rc; struct smb_fattr fattr = {{0}}; @@ -1690,6 +1697,14 @@ int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon, rc = parse_sec_desc(idmap, pntsd, ntsd_len, &fattr); if (rc) goto out; + if (fattr.cf_posix_mode && !dacl_requested) { + fattr.cf_posix_mode = false; + fattr.cf_mode = inode->i_mode; + } + if (fattr.cf_posix_mode && !posix_open) { + rc = -EOPNOTSUPP; + goto out; + } newattrs.ia_valid = ATTR_CTIME; if (!uid_eq(fattr.cf_uid, INVALID_UID)) { @@ -1701,7 +1716,12 @@ int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon, newattrs.ia_gid = fattr.cf_gid; } newattrs.ia_valid |= ATTR_MODE; - newattrs.ia_mode = (inode->i_mode & ~0777) | (fattr.cf_mode & 0777); + if (fattr.cf_posix_mode) + newattrs.ia_mode = (inode->i_mode & ~07777) | + (fattr.cf_mode & 07777); + else + newattrs.ia_mode = (inode->i_mode & ~0777) | + (fattr.cf_mode & 0777); ksmbd_vfs_remove_acl_xattrs(idmap, path); /* Update posix acls */ diff --git a/fs/smb/server/smbacl.h b/fs/smb/server/smbacl.h index 28d215807faa..3342cebab333 100644 --- a/fs/smb/server/smbacl.h +++ b/fs/smb/server/smbacl.h @@ -43,6 +43,7 @@ struct smb_fattr { kuid_t cf_uid; kgid_t cf_gid; umode_t cf_mode; + bool cf_posix_mode; __le32 daccess; struct posix_acl *cf_acls; struct posix_acl *cf_dacls; @@ -99,7 +100,8 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, 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); + bool type_check, bool get_write, bool posix_open, + bool dacl_requested); void id_to_sid(unsigned int cid, uint sidtype, struct smb_sid *ssid); void ksmbd_init_domain(u32 *sub_auth); size_t smb_acl_sec_desc_scratch_len(struct smb_fattr *fattr, From f44f9f5e9e5a6536f63a6d19e92ffb8030c37930 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:24 +0900 Subject: [PATCH 69/95] ksmbd: correct POSIX file metadata in responses POSIX responses omit special permission bits, and the create response reports a file identifier as its reparse tag without encoding the file type. Directory and file queries also report rdev as the filesystem device identifier. Report the POSIX mode and file type in create responses, include special permission bits in all three response paths, and use the special-file reparse tag consistently. Set the corresponding DOS attribute in directory and file queries, report the filesystem device identifier and initialize reserved fields. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 23 +++++++++++++++++++++-- fs/smb/server/smb2pdu.c | 19 ++++++++++++++----- fs/smb/server/smb2pdu.h | 2 ++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 234be869dd79..e76dfc904fb9 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -2569,8 +2569,27 @@ void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp) buf->Name[15] = 0x7C; buf->nlink = cpu_to_le32(inode->i_nlink); - buf->reparse_tag = cpu_to_le32(fp->volatile_id); - buf->mode = cpu_to_le32(inode->i_mode & 0777); + buf->reparse_tag = smb2_get_reparse_tag_special_file(inode->i_mode); + buf->mode = cpu_to_le32(inode->i_mode & 07777); + switch (inode->i_mode & S_IFMT) { + case S_IFDIR: + buf->mode |= cpu_to_le32(POSIX_TYPE_DIR << POSIX_FILETYPE_SHIFT); + break; + case S_IFLNK: + buf->mode |= cpu_to_le32(POSIX_TYPE_SYMLINK << POSIX_FILETYPE_SHIFT); + break; + case S_IFCHR: + buf->mode |= cpu_to_le32(POSIX_TYPE_CHARDEV << POSIX_FILETYPE_SHIFT); + break; + case S_IFBLK: + buf->mode |= cpu_to_le32(POSIX_TYPE_BLKDEV << POSIX_FILETYPE_SHIFT); + break; + case S_IFIFO: + buf->mode |= cpu_to_le32(POSIX_TYPE_FIFO << POSIX_FILETYPE_SHIFT); + break; + case S_IFSOCK: + buf->mode |= cpu_to_le32(POSIX_TYPE_SOCKET << POSIX_FILETYPE_SHIFT); + } /* * SidBuffer(44) contain two sids(Domain sid(28), UNIX group sid(16)). * Domain sid(28) = revision(1) + num_subauth(1) + authority(6) + diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 4be83ae53af2..c5acd4cde99f 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1293,7 +1293,7 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) ksmbd_free_work_struct(in_work); } -static __le32 smb2_get_reparse_tag_special_file(umode_t mode) +__le32 smb2_get_reparse_tag_special_file(umode_t mode) { if (S_ISDIR(mode) || S_ISREG(mode)) return 0; @@ -6019,6 +6019,7 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, posix_info = (struct smb2_posix_info *)kstat; posix_info->Ignored = 0; + posix_info->Zero = 0; posix_info->CreationTime = cpu_to_le64(ksmbd_kstat->create_time); time = ksmbd_UnixTimeToNT(ksmbd_kstat->kstat->ctime); posix_info->ChangeTime = cpu_to_le64(time); @@ -6028,9 +6029,11 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, posix_info->LastWriteTime = cpu_to_le64(time); posix_info->EndOfFile = cpu_to_le64(ksmbd_kstat->kstat->size); posix_info->AllocationSize = cpu_to_le64(ksmbd_kstat->kstat->blocks << 9); - posix_info->DeviceId = cpu_to_le32(ksmbd_kstat->kstat->rdev); + posix_info->DeviceId = cpu_to_le32(ksmbd_kstat->kstat->dev); posix_info->HardLinks = cpu_to_le32(ksmbd_kstat->kstat->nlink); - posix_info->Mode = cpu_to_le32(ksmbd_kstat->kstat->mode & 0777); + posix_info->ReparseTag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + posix_info->Mode = cpu_to_le32(ksmbd_kstat->kstat->mode & 07777); switch (ksmbd_kstat->kstat->mode & S_IFMT) { case S_IFDIR: posix_info->Mode |= cpu_to_le32(POSIX_TYPE_DIR << POSIX_FILETYPE_SHIFT); @@ -6055,6 +6058,8 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, posix_info->DosAttributes = S_ISDIR(ksmbd_kstat->kstat->mode) ? FILE_ATTRIBUTE_DIRECTORY_LE : FILE_ATTRIBUTE_ARCHIVE_LE; + if (posix_info->ReparseTag) + posix_info->DosAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE; if (d_info->hide_dot_file && d_info->name[0] == '.') posix_info->DosAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; /* @@ -7499,7 +7504,7 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, 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); + file_info->Mode = cpu_to_le32(stat.mode & 07777); switch (stat.mode & S_IFMT) { case S_IFDIR: file_info->Mode |= cpu_to_le32(POSIX_TYPE_DIR << POSIX_FILETYPE_SHIFT); @@ -7520,7 +7525,11 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, file_info->Mode |= cpu_to_le32(POSIX_TYPE_SOCKET << POSIX_FILETYPE_SHIFT); } - file_info->DeviceId = cpu_to_le32(stat.rdev); + file_info->DeviceId = cpu_to_le32(stat.dev); + file_info->Zero = 0; + file_info->ReparseTag = smb2_get_reparse_tag_special_file(stat.mode); + if (file_info->ReparseTag) + file_info->DosAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE; /* * Sids(32) contain two sids(Domain sid(16), UNIX group sid(16)). diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index c90a1d4e9f11..9b56ed957b96 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -168,6 +168,8 @@ struct create_posix_rsp { u8 SidBuffer[44]; } __packed; +__le32 smb2_get_reparse_tag_special_file(umode_t mode); + #define SMB2_0_IOCTL_IS_FSCTL 0x00000001 struct sockaddr_storage_rsp { From 08ca2d56eefab606039719c9b71c5891461b48e1 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:24 +0900 Subject: [PATCH 70/95] ksmbd: include filenames in POSIX query info The POSIX file query response ends after the owner and group SIDs, omitting the filename length and filename. Append the path relative to the share in UTF-16, without a leading separator, and include its byte length in the response. Check both the fixed fields and the converted filename against the available output buffer before writing the response. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 56 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index c5acd4cde99f..a795974f6726 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7462,8 +7462,10 @@ static int get_file_attribute_tag_info(struct smb2_query_info_rsp *rsp, return 0; } -static int find_file_posix_info(struct smb2_query_info_rsp *rsp, - struct ksmbd_file *fp, void *rsp_org) +static int find_file_posix_info(struct ksmbd_work *work, + struct smb2_query_info_req *req, + struct smb2_query_info_rsp *rsp, + struct ksmbd_file *fp) { struct smb311_posix_qinfo *file_info; struct inode *inode = file_inode(fp->filp); @@ -7471,9 +7473,13 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode); vfsgid_t vfsgid = i_gid_into_vfsgid(idmap, inode); struct kstat stat; + char *filename; + __le16 *utf16_name; + __le32 *filename_len; + size_t name_bytes; u64 time; - int out_buf_len = sizeof(struct smb311_posix_qinfo) + 32; - int ret; + int fixed_len = sizeof(struct smb311_posix_qinfo) + 32 + sizeof(__le32); + int max_len, conv_len, ret; if (!(fp->daccess & FILE_READ_ATTRIBUTES_LE)) { pr_err("no right to read the attributes : 0x%x\n", @@ -7484,6 +7490,38 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, ret = get_file_allocation_stat(fp, &stat); if (ret) return ret; + max_len = smb2_calc_max_out_buf_len(work, + offsetof(struct smb2_query_info_rsp, Buffer), + le32_to_cpu(req->OutputBufferLength)); + if (max_len < fixed_len) { + rsp->hdr.Status = STATUS_INFO_LENGTH_MISMATCH; + return -EINVAL; + } + 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)); + name_bytes = strlen(filename); + utf16_name = kmalloc_array(name_bytes + 1, sizeof(*utf16_name), + KSMBD_DEFAULT_GFP); + if (!utf16_name) { + kfree(filename); + return -ENOMEM; + } + conv_len = smbConvertToUTF16(utf16_name, filename, name_bytes, + work->conn->local_nls, 0); + kfree(filename); + if (conv_len < 0) { + kfree(utf16_name); + return conv_len; + } + if (conv_len > (max_len - fixed_len) / sizeof(*utf16_name)) { + kfree(utf16_name); + rsp->hdr.Status = STATUS_INFO_LENGTH_MISMATCH; + return -EINVAL; + } file_info = (struct smb311_posix_qinfo *)rsp->Buffer; file_info->CreationTime = cpu_to_le64(fp->create_time); @@ -7540,8 +7578,14 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, SIDUNIX_USER, (struct smb_sid *)&file_info->Sids[0]); id_to_sid(from_kgid_munged(&init_user_ns, vfsgid_into_kgid(vfsgid)), SIDUNIX_GROUP, (struct smb_sid *)&file_info->Sids[16]); + filename_len = (__le32 *)&file_info->Sids[32]; + memcpy(&file_info->Sids[36], utf16_name, + conv_len * sizeof(*utf16_name)); + kfree(utf16_name); + *filename_len = cpu_to_le32(conv_len * sizeof(__le16)); - rsp->OutputBufferLength = cpu_to_le32(out_buf_len); + rsp->OutputBufferLength = cpu_to_le32(fixed_len + + conv_len * sizeof(__le16)); return 0; } @@ -7651,7 +7695,7 @@ static int smb2_get_info_file(struct ksmbd_work *work, pr_err("client doesn't negotiate with SMB3.1.1 POSIX Extensions\n"); rc = -EOPNOTSUPP; } else { - rc = find_file_posix_info(rsp, fp, work->response_buf); + rc = find_file_posix_info(work, req, rsp, fp); } break; default: From 628777005a484b6a437774e8d305626c7121b47e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:24 +0900 Subject: [PATCH 71/95] ksmbd: require POSIX opens for file and directory queries POSIX file information queries only check the tree's extension flag, while directory queries do not verify that the directory was opened with a POSIX create context. Require SMB3.1.1, negotiated POSIX tree extensions and a POSIX open for both file and directory POSIX information requests. Return STATUS_INVALID_INFO_CLASS when these conditions are not met. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a795974f6726..128c09e6d36e 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6469,6 +6469,12 @@ int smb2_query_dir(struct ksmbd_work *work) rc = -EBADF; goto err_out2; } + if (req->FileInformationClass == SMB_FIND_FILE_POSIX_INFO && + (conn->dialect != SMB311_PROT_ID || + !work->tcon->posix_extensions || !dir_fp->is_posix_ctxt)) { + rc = -EFAULT; + goto err_out2; + } if (!(dir_fp->daccess & FILE_LIST_DIRECTORY_LE) || inode_permission(file_mnt_idmap(dir_fp->filp), @@ -7691,8 +7697,9 @@ static int smb2_get_info_file(struct ksmbd_work *work, rc = get_file_attribute_tag_info(rsp, fp, work->response_buf); break; case SMB_FIND_FILE_POSIX_INFO: - if (!work->tcon->posix_extensions) { - pr_err("client doesn't negotiate with SMB3.1.1 POSIX Extensions\n"); + if (work->conn->dialect != SMB311_PROT_ID || + !work->tcon->posix_extensions || !fp->is_posix_ctxt) { + rsp->hdr.Status = STATUS_INVALID_INFO_CLASS; rc = -EOPNOTSUPP; } else { rc = find_file_posix_info(work, req, rsp, fp); From 497e3585ecaed9796ff3f02cab2c0dad3e5fb950 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:24 +0900 Subject: [PATCH 72/95] ksmbd: query POSIX filesystem info through the open file POSIX filesystem queries use the share root for statfs and do not validate the requested file handle. This can report the wrong filesystem for files below a mount point within the share. Require SMB3.1.1 POSIX extensions and a valid POSIX open. Resolve the requested handle, including compound file IDs, and use its referenced path for the filesystem query. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 73 ++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 128c09e6d36e..59312ec188f2 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7746,20 +7746,47 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, { struct ksmbd_conn *conn = work->conn; struct ksmbd_share_config *share = work->tcon->share_conf; + struct ksmbd_file *fp; int fsinfoclass = 0; struct kstatfs stfs; struct path path; int rc = 0, len; unsigned int fixed_len = 0; + u64 id, pid; - if (!share->path) - return -EIO; + if (req->FileInfoClass == FS_POSIX_INFORMATION) { + if (conn->dialect != SMB311_PROT_ID || + !work->tcon->posix_extensions) { + rsp->hdr.Status = STATUS_INVALID_INFO_CLASS; + return -EOPNOTSUPP; + } + id = req->VolatileFileId; + pid = req->PersistentFileId; + if (work->next_smb2_rcv_hdr_off && !has_file_id(id)) { + id = work->compound_fid; + pid = work->compound_pfid; + } + fp = ksmbd_lookup_fd_slow(work, id, pid); + if (!fp) + return -ENOENT; + if (!fp->is_posix_ctxt) { + ksmbd_fd_put(work, fp); + rsp->hdr.Status = STATUS_INVALID_INFO_CLASS; + return -EOPNOTSUPP; + } + path = fp->filp->f_path; + path_get(&path); + ksmbd_fd_put(work, fp); + } else { + if (!share->path) + return -EIO; - scoped_with_init_fs() - rc = kern_path(share->path, LOOKUP_NO_SYMLINKS, &path); - if (rc) { - pr_err("cannot create vfs path\n"); - return -EIO; + scoped_with_init_fs() + rc = kern_path(share->path, LOOKUP_NO_SYMLINKS, &path); + if (rc) { + pr_err("cannot create vfs path\n"); + return -EIO; + } } rc = vfs_statfs(&path, &stfs); @@ -7972,25 +7999,19 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, { FILE_SYSTEM_POSIX_INFO *info; - if (!work->tcon->posix_extensions) { - pr_err("client doesn't negotiate with SMB3.1.1 POSIX Extensions\n"); - path_put(&path); - return -EOPNOTSUPP; - } else { - info = (FILE_SYSTEM_POSIX_INFO *)(rsp->Buffer); - info->OptimalTransferSize = cpu_to_le32(stfs.f_bsize); - info->BlockSize = cpu_to_le32(stfs.f_bsize); - info->TotalBlocks = cpu_to_le64(stfs.f_blocks); - info->BlocksAvail = cpu_to_le64(stfs.f_bfree); - info->UserBlocksAvail = cpu_to_le64(stfs.f_bavail); - info->TotalFileNodes = cpu_to_le64(stfs.f_files); - info->FreeFileNodes = cpu_to_le64(stfs.f_ffree); - info->FileSysIdentifier = - cpu_to_le64((u64)(u32)stfs.f_fsid.val[1] << 32 | - (u32)stfs.f_fsid.val[0]); - rsp->OutputBufferLength = cpu_to_le32(56); - fixed_len = 56; - } + info = (FILE_SYSTEM_POSIX_INFO *)(rsp->Buffer); + info->OptimalTransferSize = cpu_to_le32(stfs.f_bsize); + info->BlockSize = cpu_to_le32(stfs.f_bsize); + info->TotalBlocks = cpu_to_le64(stfs.f_blocks); + info->BlocksAvail = cpu_to_le64(stfs.f_bfree); + info->UserBlocksAvail = cpu_to_le64(stfs.f_bavail); + info->TotalFileNodes = cpu_to_le64(stfs.f_files); + info->FreeFileNodes = cpu_to_le64(stfs.f_ffree); + info->FileSysIdentifier = + cpu_to_le64((u64)(u32)stfs.f_fsid.val[1] << 32 | + (u32)stfs.f_fsid.val[0]); + rsp->OutputBufferLength = cpu_to_le32(56); + fixed_len = 56; break; } default: From 7f6c8ef1948af0eb7ffbe0fe2f73736b91861b8d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 26 Sep 2026 22:48:24 +0900 Subject: [PATCH 73/95] ksmbd: correct POSIX filesystem block size and response length POSIX filesystem information uses f_bsize for BlockSize even when the filesystem reports a distinct fragment size for its block counts. Use f_frsize when available, falling back to f_bsize. Keep f_bsize as the optimal transfer size and derive the response length from the information structure instead of a hard-coded value. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 59312ec188f2..7efd60c36431 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8001,7 +8001,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info = (FILE_SYSTEM_POSIX_INFO *)(rsp->Buffer); info->OptimalTransferSize = cpu_to_le32(stfs.f_bsize); - info->BlockSize = cpu_to_le32(stfs.f_bsize); + info->BlockSize = cpu_to_le32(stfs.f_frsize ?: stfs.f_bsize); info->TotalBlocks = cpu_to_le64(stfs.f_blocks); info->BlocksAvail = cpu_to_le64(stfs.f_bfree); info->UserBlocksAvail = cpu_to_le64(stfs.f_bavail); @@ -8010,8 +8010,8 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->FileSysIdentifier = cpu_to_le64((u64)(u32)stfs.f_fsid.val[1] << 32 | (u32)stfs.f_fsid.val[0]); - rsp->OutputBufferLength = cpu_to_le32(56); - fixed_len = 56; + rsp->OutputBufferLength = cpu_to_le32(sizeof(*info)); + fixed_len = sizeof(*info); break; } default: From ef85bad39cf273a8377722c9f3078294dcc2fc7a Mon Sep 17 00:00:00 2001 From: Lee Seong Hyeon Date: Thu, 15 Oct 2026 18:07:03 +0700 Subject: [PATCH 74/95] ksmbd: reject FSCTL_SET_ZERO_DATA on a directory handle An authenticated client with a writable tree connection can open a directory with FILE_WRITE_DATA and send FSCTL_SET_ZERO_DATA on that handle. The handler only checks fp->daccess & FILE_WRITE_DATA_LE, but on a directory FILE_WRITE_DATA is the legitimate add-file right, so the check passes. ksmbd_vfs_zero_data() then calls kernel_write() on the directory file, which has no FMODE_WRITE, and __kernel_write_iter() hits WARN_ON_ONCE(!(file->f_mode & FMODE_WRITE)), panicking a panic_on_warn kernel. Before commit 25e414db7033 ("ksmbd: report holes in allocated range queries") the dense-file path used vfs_fallocate(), which rejects directories with -EISDIR. The kernel_write() loop that replaced it has no such check. Reject a directory handle in ksmbd_vfs_zero_data() before writing, as ksmbd_vfs_write() already does for SMB2_WRITE. WARNING: fs/read_write.c:608 at __kernel_write_iter+0x74b/0x8c0 Workqueue: ksmbd-io handle_ksmbd_work Call Trace: kernel_write+0x1ef/0x4d0 ksmbd_vfs_zero_data+0x413/0x590 smb2_ioctl+0x21fa/0x66a0 handle_ksmbd_work+0x61b/0x1ff0 Fixes: 25e414db7033 ("ksmbd: report holes in allocated range queries") Signed-off-by: Lee Seong Hyeon Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index a1d107e77b34..5360c6999bb3 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1012,6 +1012,15 @@ int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, char *zero_buf = NULL; int err; + /* + * A directory handle can hold FILE_WRITE_DATA (the add-file right), + * so the caller access check does not exclude it. Writing zeroes to a + * directory makes no sense and kernel_write() would warn on the + * directory file (no FMODE_WRITE), so reject it here. + */ + if (S_ISDIR(file_inode(fp->filp)->i_mode)) + return -EINVAL; + smb_break_all_levII_oplock(work, fp, 1); if (!work->tcon->posix_extensions) { loff_t size = i_size_read(file_inode(fp->filp)); From a025c4991a7d83edc4dd339e3ea86d8ff6226eca Mon Sep 17 00:00:00 2001 From: Abdifatah Suruur Date: Sat, 17 Oct 2026 13:54:54 +0300 Subject: [PATCH 75/95] ksmbd: reserve space for the UTF-16 terminator in stream info smb_strtoUTF16() always writes a terminating zero UTF-16 unit after the converted characters, but get_file_stream_info() budgets only sizeof(struct smb2_file_stream_info) + streamlen * 2 for a stream entry and sizeof(struct smb2_file_stream_info) + 7 * 2 for the trailing "::$DATA" entry. When an entry exactly fills the 448-byte small response buffer, the terminator is written two bytes past the allocation. Reserve one extra UTF-16 unit in both budget checks. The terminator is not part of the wire format, so the reported lengths are unchanged. Cc: stable@vger.kernel.org Signed-off-by: Abdifatah Suruur Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 7efd60c36431..a1b9368504f9 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7274,7 +7274,8 @@ static int get_file_stream_info(struct ksmbd_work *work, ":%s", &stream_name[XATTR_NAME_STREAM_LEN]); next = sizeof(struct smb2_file_stream_info) + streamlen * 2; - if (next > buf_free_len) { + /* smbConvertToUTF16() also writes a terminating UTF-16 unit. */ + if (next + sizeof(__le16) > buf_free_len) { kfree(stream_buf); break; } @@ -7299,7 +7300,7 @@ static int get_file_stream_info(struct ksmbd_work *work, out: if (!S_ISDIR(stat.mode) && - buf_free_len >= sizeof(struct smb2_file_stream_info) + 7 * 2) { + buf_free_len >= sizeof(struct smb2_file_stream_info) + (7 + 1) * 2) { file_info = (struct smb2_file_stream_info *) &rsp->Buffer[nbytes]; streamlen = smbConvertToUTF16((__le16 *)file_info->StreamName, From 318cd7461a7facb9cd5f7a5d1580fd874440e035 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 18 Oct 2026 00:00:00 +0900 Subject: [PATCH 76/95] ksmbd: inherit FileIds in related compound requests MS-SMB2 specifies that subsequent related operations use the FileId from the preceding operation. Track FileIds from handle-bearing requests so related requests that use the all-ones placeholder resolve to the inherited handle. Return STATUS_INVALID_HANDLE when a related request refers to a FileId but the preceding operation neither contained nor generated one. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Reported-by: Ronnie Sahlberg Signed-off-by: Namjae Jeon --- fs/smb/server/ksmbd_work.h | 9 ++- fs/smb/server/server.c | 5 +- fs/smb/server/smb2pdu.c | 150 ++++++++++++++++++++++++++++--------- fs/smb/server/smb2pdu.h | 1 + 4 files changed, 126 insertions(+), 39 deletions(-) diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index b51dea69cb0e..1ab9ddd20506 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -54,10 +54,7 @@ struct ksmbd_work { /* Current cmd hdr in compound rsp buf*/ int curr_smb2_rsp_hdr_off; - /* - * Current Local FID assigned compound response if SMB2 CREATE - * command is present in compound request - */ + /* FileId generated or used by the previous operation in a compound. */ u64 compound_fid; u64 compound_pfid; u64 compound_sid; @@ -93,6 +90,10 @@ struct ksmbd_work { bool asynchronous:1; bool need_invalidate_rkey:1; bool request_open_chseq_tracked:1; + /* The previous command carries a FileId or is a CREATE. */ + bool compound_fid_present:1; + /* A related command failed because there was no FileId to inherit. */ + bool compound_fid_missing:1; bool session_setup_reauth:1; /* Retain the binding decision for response signing and preauth hashing. */ bool session_setup_binding:1; diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index effc539548dc..f9ddbc253595 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -157,7 +157,10 @@ static int __process_request(struct ksmbd_work *work, struct ksmbd_conn *conn, } } - ret = cmds->proc(work); + if (smb2_check_compound_fid(work)) + ret = -EBADF; + else + ret = cmds->proc(work); if (conn->ops->inc_reqs) { struct smb2_hdr *rsp = ksmbd_resp_buf_curr(work); diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a1b9368504f9..1a0a8cff33bb 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -440,6 +440,95 @@ int smb2_set_rsp_credits(struct ksmbd_work *work) return 0; } +static bool smb2_get_file_ids(struct smb2_hdr *hdr, u64 *volatile_id, + u64 *persistent_id) +{ + unsigned int offset; + + switch (hdr->Command) { + case SMB2_CLOSE: + offset = offsetof(struct smb2_close_req, PersistentFileId); + break; + case SMB2_FLUSH: + offset = offsetof(struct smb2_flush_req, PersistentFileId); + break; + case SMB2_READ: + offset = offsetof(struct smb2_read_req, PersistentFileId); + break; + case SMB2_WRITE: + offset = offsetof(struct smb2_write_req, PersistentFileId); + break; + case SMB2_LOCK: + offset = offsetof(struct smb2_lock_req, PersistentFileId); + break; + case SMB2_IOCTL: { + struct smb2_ioctl_req *req = (struct smb2_ioctl_req *)hdr; + + switch (le32_to_cpu(req->CtlCode)) { + 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: + return false; + } + offset = offsetof(struct smb2_ioctl_req, PersistentFileId); + break; + } + case SMB2_QUERY_DIRECTORY: + offset = offsetof(struct smb2_query_directory_req, PersistentFileId); + break; + case SMB2_CHANGE_NOTIFY: + offset = offsetof(struct smb2_change_notify_req, PersistentFileId); + break; + case SMB2_QUERY_INFO: + offset = offsetof(struct smb2_query_info_req, PersistentFileId); + break; + case SMB2_SET_INFO: + offset = offsetof(struct smb2_set_info_req, PersistentFileId); + break; + case SMB2_OPLOCK_BREAK: + if (((struct smb2_oplock_break *)hdr)->StructureSize != + cpu_to_le16(OP_BREAK_STRUCT_SIZE_20)) + return false; + offset = offsetof(struct smb2_oplock_break, PersistentFid); + break; + default: + return false; + } + + /* FileId contains consecutive persistent and volatile opaque values. */ + memcpy(persistent_id, (char *)hdr + offset, sizeof(*persistent_id)); + memcpy(volatile_id, (char *)hdr + offset + sizeof(*persistent_id), + sizeof(*volatile_id)); + return true; +} + +/* Called after message validation and signature verification. */ +bool smb2_check_compound_fid(struct ksmbd_work *work) +{ + struct smb2_hdr *req = ksmbd_req_buf_next(work); + struct smb2_hdr *rsp = ksmbd_resp_buf_next(work); + u64 volatile_id, persistent_id; + + if (!work->next_smb2_rcv_hdr_off || + !(req->Flags & SMB2_FLAGS_RELATED_OPERATIONS)) + return false; + + if (!work->compound_fid_missing && + !work->compound_fid_present && + smb2_get_file_ids(req, &volatile_id, &persistent_id)) + work->compound_fid_missing = true; + + if (!work->compound_fid_missing) + return false; + + /* MS-SMB2 3.3.5.2.7.2 requires all subsequent operations to fail too. */ + rsp->Status = STATUS_INVALID_HANDLE; + smb2_set_err_rsp(work); + return true; +} + /** * init_chained_smb2_rsp() - initialize smb2 chained response * @work: smb work containing smb response buffer @@ -452,6 +541,21 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) struct smb2_hdr *rcv_hdr; int next_hdr_offset = 0; int len, new_len; + u64 volatile_id = KSMBD_NO_FID, persistent_id = KSMBD_NO_FID; + bool req_has_fid; + + req_has_fid = smb2_get_file_ids(req, &volatile_id, &persistent_id); + /* A failed CREATE is handled through compound_status instead. */ + work->compound_fid_present = req_has_fid || req->Command == SMB2_CREATE; + if (!work->compound_fid_present) { + work->compound_fid = KSMBD_NO_FID; + work->compound_pfid = KSMBD_NO_FID; + } + + /* Related requests keep the inherited SessionId. */ + if (!work->next_smb2_rcv_hdr_off || + !(req->Flags & SMB2_FLAGS_RELATED_OPERATIONS)) + work->compound_sid = le64_to_cpu(rsp->SessionId); /* Len of this response = updated RFC len - offset of previous cmd * in the compound rsp @@ -463,48 +567,24 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) if (req->Command == SMB2_CREATE && rsp->Status == STATUS_SUCCESS) { work->compound_fid = ((struct smb2_create_rsp *)rsp)->VolatileFileId; work->compound_pfid = ((struct smb2_create_rsp *)rsp)->PersistentFileId; - work->compound_sid = le64_to_cpu(rsp->SessionId); work->compound_status = STATUS_SUCCESS; - } else if ((req->Command == SMB2_FLUSH || - req->Command == SMB2_READ || - req->Command == SMB2_WRITE) && - rsp->Status == STATUS_SUCCESS) { - u64 volatile_id = KSMBD_NO_FID; - u64 persistent_id = KSMBD_NO_FID; - - if (req->Command == SMB2_FLUSH) { - struct smb2_flush_req *flush_req = - (struct smb2_flush_req *)req; - - volatile_id = flush_req->VolatileFileId; - persistent_id = flush_req->PersistentFileId; - } else if (req->Command == SMB2_READ) { - struct smb2_read_req *read_req = - (struct smb2_read_req *)req; - - volatile_id = read_req->VolatileFileId; - persistent_id = read_req->PersistentFileId; + } else if (rsp->Status == STATUS_SUCCESS) { + if (req_has_fid && req->Command != SMB2_CLOSE) { + /* Preserve the inherited FID when the request uses all ones. */ + if (has_file_id(volatile_id)) { + work->compound_fid = volatile_id; + work->compound_pfid = persistent_id; + } } else { - struct smb2_write_req *write_req = - (struct smb2_write_req *)req; - - volatile_id = write_req->VolatileFileId; - persistent_id = write_req->PersistentFileId; - } - - if (has_file_id(volatile_id)) { - work->compound_fid = volatile_id; - work->compound_pfid = persistent_id; - work->compound_sid = le64_to_cpu(rsp->SessionId); - work->compound_status = STATUS_SUCCESS; + work->compound_fid = KSMBD_NO_FID; + work->compound_pfid = KSMBD_NO_FID; } + work->compound_status = STATUS_SUCCESS; } else if (req->Command == SMB2_CREATE) { work->compound_fid = KSMBD_NO_FID; work->compound_pfid = KSMBD_NO_FID; - work->compound_sid = le64_to_cpu(rsp->SessionId); work->compound_status = rsp->Status; } else if (rsp->Status != STATUS_SUCCESS) { - work->compound_sid = le64_to_cpu(rsp->SessionId); /* * Only carry the failed status forward when the failing command * was itself part of the related chain. An unrelated command @@ -543,6 +623,8 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) work->compound_fid = KSMBD_NO_FID; work->compound_pfid = KSMBD_NO_FID; work->compound_status = STATUS_SUCCESS; + work->compound_fid_present = false; + work->compound_fid_missing = false; } memset((char *)rsp_hdr, 0, sizeof(struct smb2_hdr) + 2); rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER; diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index 9b56ed957b96..8c6b170c2716 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -395,6 +395,7 @@ bool is_smb2_rsp(struct ksmbd_work *work); u16 get_smb2_cmd_val(struct ksmbd_work *work); void set_smb2_rsp_status(struct ksmbd_work *work, __le32 err); int init_smb2_rsp_hdr(struct ksmbd_work *work); +bool smb2_check_compound_fid(struct ksmbd_work *work); int smb2_allocate_rsp_buf(struct ksmbd_work *work); bool is_chained_smb2_message(struct ksmbd_work *work); int init_smb2_neg_rsp(struct ksmbd_work *work); From 9455e920aebe41217cabff3f81e90da4c407fdb6 Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Sat, 17 Oct 2026 12:40:50 +0900 Subject: [PATCH 77/95] ksmbd: avoid restoring only in-memory EOF after allocation change When SetAllocation requests a size below the allocated block count but above EOF, ksmbd truncates the file to that size and restores only the in-memory i_size. The larger EOF remains on disk and reappears after a reboot. A 3-byte file preallocated to 64 KiB can acquire a 4 KiB EOF after reducing its allocation to 4 KiB. Extending EOF this way is not a reliable way to reclaim the preallocation beyond the old EOF either. Only use this truncate path when the requested allocation is at or below the observed EOF. A truncate to the existing EOF still lets filesystems release preallocated blocks beyond EOF. Remove the in-memory-only i_size restoration. The size check still precedes vfs_truncate() taking the inode lock, so a concurrent shrink can make the truncate extend EOF; that race also existed in the original path. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Signed-off-by: DaeMyung Kang Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 1a0a8cff33bb..9c6345e63434 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8788,24 +8788,17 @@ static int set_file_allocation_info(struct ksmbd_work *work, pr_err("vfs_fallocate is failed : %d\n", rc); return rc; } - } else if (alloc_blks < stat.blocks) { - loff_t size; - + } else if (alloc_blks < stat.blocks && + alloc_blks * 512 <= i_size_read(inode)) { /* - * Allocation size could be smaller than original one - * which means allocated blocks in file should be - * deallocated. use truncate to cut out it, but inode - * size is also updated with truncate offset. - * inode size is retained by backup inode size. + * Truncate at or below EOF to discard excess allocation. + * Truncating above EOF would extend the file on disk. */ - size = i_size_read(inode); rc = ksmbd_vfs_truncate(work, fp, alloc_blks * 512); if (rc) { pr_err("truncate failed!, err %d\n", rc); return rc; } - if (size < alloc_blks * 512) - i_size_write(inode, size); } fp->allocation_size = le64_to_cpu(file_alloc_info->AllocationSize); From aad7d86a6651efefb2c0a4f02c86dfeff3c176a2 Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Sat, 17 Oct 2026 12:40:51 +0900 Subject: [PATCH 78/95] ksmbd: round SetAllocation to the advertised allocation unit SetAllocation rounds requests to 512-byte blocks, although FS_SIZE_INFORMATION advertises the filesystem's f_bsize as its allocation unit. For example, on a filesystem with a 4 KiB unit, a 1000-byte request can truncate an 8 KiB file to 1024 bytes instead of 4 KiB. Round the request to the advertised unit and use the same result for the block-count decision, fallocate, truncate, and the size stored on the open handle. Validate the unit and rounded size before either operation. Use DIV_ROUND_UP_ULL() so the calculation works on 32-bit kernels. This does not change the existing block-count decision for whether to call fallocate. In particular, success does not yet guarantee reservation of holes within the requested range on a sparse file. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: DaeMyung Kang Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 9c6345e63434..1ffff3e6eb28 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8748,10 +8748,11 @@ static int set_file_allocation_info(struct ksmbd_work *work, * properly with any smb.conf option */ - loff_t alloc_blks; - u64 alloc_size; + u64 alloc_size, target, alloc_blks; + u32 alloc_unit; struct inode *inode; struct kstat stat; + struct kstatfs stfs; int rc; if (!(fp->daccess & FILE_WRITE_DATA_LE)) @@ -8764,44 +8765,48 @@ static int set_file_allocation_info(struct ksmbd_work *work, AT_STATX_SYNC_AS_STAT); if (rc) return rc; + rc = vfs_statfs(&fp->filp->f_path, &stfs); + if (rc) + return rc; /* - * AllocationSize is fully client-controlled (the caller only - * validates the fixed 8-byte buffer length). Reject values that - * would overflow the "round up to 512-byte blocks" conversion - * below instead of silently wrapping it to a tiny block count, - * which would truncate the file to a size the client never - * asked for. + * Use the allocation unit advertised in FS_SIZE_INFORMATION. + * Reject requests that overflow when rounded to a whole unit. */ alloc_size = le64_to_cpu(file_alloc_info->AllocationSize); - if (alloc_size > MAX_LFS_FILESIZE - 511) + if (stfs.f_bsize <= 0 || (u64)stfs.f_bsize > U32_MAX) + return -EINVAL; + alloc_unit = stfs.f_bsize; + if (alloc_size > MAX_LFS_FILESIZE - (alloc_unit - 1)) return -EINVAL; - alloc_blks = (alloc_size + 511) >> 9; + target = DIV_ROUND_UP_ULL(alloc_size, alloc_unit) * alloc_unit; + alloc_blks = DIV_ROUND_UP_ULL(target, 512); inode = file_inode(fp->filp); + if (target > inode->i_sb->s_maxbytes) + return -EINVAL; if (alloc_blks > stat.blocks) { smb_break_all_levII_oplock(work, fp, 1); - rc = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0, - alloc_blks * 512); + rc = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0, target); if (rc && rc != -EOPNOTSUPP) { pr_err("vfs_fallocate is failed : %d\n", rc); return rc; } } else if (alloc_blks < stat.blocks && - alloc_blks * 512 <= i_size_read(inode)) { + target <= i_size_read(inode)) { /* * Truncate at or below EOF to discard excess allocation. * Truncating above EOF would extend the file on disk. */ - rc = ksmbd_vfs_truncate(work, fp, alloc_blks * 512); + rc = ksmbd_vfs_truncate(work, fp, target); if (rc) { pr_err("truncate failed!, err %d\n", rc); return rc; } } - fp->allocation_size = le64_to_cpu(file_alloc_info->AllocationSize); + fp->allocation_size = target; fp->allocation_size_set = true; return 0; } From 4f1ce9ffb996a524acf864316479a7806ae500c6 Mon Sep 17 00:00:00 2001 From: Michail Andreas Michalopoulos Date: Mon, 19 Oct 2026 21:11:16 +0200 Subject: [PATCH 79/95] ksmbd: accept share config responses from ksmbd-tools before 3.5.3 ksmbd-tools releases before 3.5.2 do not have the payload_sz field in struct ksmbd_share_config_response; the field occupies the last word of their reserved[] area, so the kernel always sees payload_sz == 0 from them. ipc_validate_msg() and share_config_request() used to treat payload_sz == 0 as "legacy userspace" and skip the payload size checks. That case was removed by commit f25e93768fcc ("ksmbd: prevent out-of-bounds reads in share config responses"). With payload_sz == 0: - the IPC$ (pipe) response fails the final message size comparison: these tools always send a one-byte payload, so msg_sz is sizeof(*resp) + 1, but the expected size is now computed as sizeof(*resp) + 0; - every other share fails "payload_sz <= veto_list_sz". In addition, ksmbd-tools before 3.5.3 copy a global "veto files" setting into the IPC$ response, so with such a configuration IPC$ also fails the now unconditional "veto_list_sz > payload_sz" check, even though pipe shares never use a veto list. ksmbd_ipc_share_config_request() then returns NULL and every tree connect fails with STATUS_BAD_NETWORK_NAME, so ksmbd cannot serve any share when used with these ksmbd-tools releases, for example the 3.5.1 package shipped by Ubuntu 24.04. Login still works, which makes the failure look like a configuration problem: CIFS: VFS: BAD_NETWORK_NAME: \\127.0.0.1\IPC$ CIFS: VFS: \\127.0.0.1 failed to connect to IPC (rc=-2) When payload_sz is zero, derive it from the size of the message actually received and store it back in the response, so that the checks in ipc_validate_msg() and the path and veto list parsing in share_config_request() all use the same bound. The response is the kernel's own copy made by handle_response(), and msg_sz has already been checked to be at least sizeof(*resp), so this cannot underflow. All checks added by that commit then apply unchanged, bounded by the bytes that really arrived, and newer ksmbd-tools that fill in payload_sz are unaffected. Also skip the veto list size check for pipe shares: share_config_request() reads neither the path nor the veto list for them. Tested in QEMU with ksmbd-tools 3.5.1 and 3.5.7 and the in-kernel SMB client over loopback, with and without a global "veto files" setting. Fixes: f25e93768fcc ("ksmbd: prevent out-of-bounds reads in share config responses") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Michail Andreas Michalopoulos Signed-off-by: Namjae Jeon --- fs/smb/server/transport_ipc.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c index e550aa41ad2c..f5cfc320500e 100644 --- a/fs/smb/server/transport_ipc.c +++ b/fs/smb/server/transport_ipc.c @@ -532,11 +532,25 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry) if (entry->msg_sz < sizeof(struct ksmbd_share_config_response)) return -EINVAL; + /* + * ksmbd-tools releases before 3.5.2 leave payload_sz zero. + * Derive it from the size of the message actually received so + * the checks below bound the payload for those releases too. + */ + if (!resp->payload_sz) + resp->payload_sz = entry->msg_sz - + sizeof(struct ksmbd_share_config_response); + if (strnlen(resp->share_name, sizeof(resp->share_name)) == sizeof(resp->share_name)) return -EINVAL; - if (resp->veto_list_sz > resp->payload_sz) + /* + * Pipe shares carry no veto list, but ksmbd-tools before 3.5.3 + * copy a global "veto files" setting into the IPC$ response. + */ + if (!(resp->flags & KSMBD_SHARE_FLAG_PIPE) && + resp->veto_list_sz > resp->payload_sz) return -EINVAL; if (resp->flags != KSMBD_SHARE_FLAG_INVALID && From b4bae7af1154089be87c7475218264fcbfd16d10 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 16 Oct 2026 23:17:36 +0900 Subject: [PATCH 80/95] ksmbd: defer connection scavenging while session locks are busy The expiration worker holds conn_list_lock for reading while checking for authenticated sessions. Without this change, the check can wait for conn->session_lock or sessions_table_lock. In destroy_previous_session(), the status updates after ksmbd_conn_wait_idle_sess() run with both session locks held for writing. Both the KSMBD_SESS_GOOD error path and the KSMBD_SESS_NEED_SETUP success path call ksmbd_all_conn_set_status(), which takes conn_list_lock for reading. A writer queued on conn_list_lock can block these new read acquisitions. The worker then waits for a session lock held by destroy_previous_session(), which waits behind the writer. The writer cannot proceed until the worker releases conn_list_lock. Try both session locks instead of waiting in the scavenging check. Keep the connection when either lock is busy and let the periodic worker retry later. Fixes: d7fd1f98607f ("ksmbd: follow SMB2 session expiration semantics") Cc: stable@vger.kernel.org Reported-by: Abdifatah Suruur Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 554434c7673a..cae555b5f722 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -532,7 +532,12 @@ bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn) int state, bkt; bool found = false; - down_read(&conn->session_lock); + /* + * The expiration worker holds conn_list_lock, which session teardown + * takes after the session locks. Defer scavenging if either is busy. + */ + if (!down_read_trylock(&conn->session_lock)) + return true; xa_for_each(&conn->sessions, id, sess) { state = READ_ONCE(sess->state); if (state == SMB2_SESSION_VALID || @@ -546,7 +551,8 @@ bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn) return true; /* A session bound through SMB3 multichannel is not in conn->sessions. */ - down_read(&sessions_table_lock); + if (!down_read_trylock(&sessions_table_lock)) + return true; hash_for_each(sessions_table, bkt, sess, hlist) { state = READ_ONCE(sess->state); if (state != SMB2_SESSION_VALID && From add8d7fb04efe564193df73581889c62fc0016f1 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 16 Oct 2026 23:17:58 +0900 Subject: [PATCH 81/95] ksmbd: hold a connection reference for each session channel ksmbd_session_unregister() removes a session from the global and per- connection tables before dropping the table reference. A concurrent reference holder can keep the session and its channel list alive after those removals. Connection cleanup can no longer find the unpublished session to remove its channels. Since a channel only stores a raw conn pointer, the connection can be freed while another session reference holder is still accessing it, for example during SMB3.1.1 binding or a procfs read. Take a connection reference when registering a channel and release it on insertion failure, channel removal, or final session destruction. Keep the channels and signing keys until the session is destroyed so a response can still be signed after the session has been unregistered. Fixes: d7fd1f98607f ("ksmbd: follow SMB2 session expiration semantics") Cc: stable@vger.kernel.org Reported-by: Abdifatah Suruur Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 2 ++ fs/smb/server/smb2pdu.c | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index cae555b5f722..8314e4f0d534 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -271,6 +271,7 @@ static void free_channel_list(struct ksmbd_session *sess) down_write(&sess->chann_lock); xa_for_each(&sess->ksmbd_chann_list, index, chann) { xa_erase(&sess->ksmbd_chann_list, index); + ksmbd_conn_put(chann->conn); kfree_sensitive(chann); } @@ -598,6 +599,7 @@ static int ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess) if (!chann) return -ENOENT; + ksmbd_conn_put(chann->conn); kfree_sensitive(chann); return 0; } diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 1ffff3e6eb28..dbda6dea061e 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -113,11 +113,12 @@ static int register_session_channel(struct ksmbd_session *sess, goto out; } - chann->conn = conn; + chann->conn = ksmbd_conn_get(conn); memcpy(chann->sess_key, sess_key, sizeof(chann->sess_key)); old = xa_store(&sess->ksmbd_chann_list, (long)conn, chann, KSMBD_DEFAULT_GFP); if (xa_is_err(old)) { + ksmbd_conn_put(chann->conn); kfree_sensitive(chann); rc = xa_err(old); } From dc6aa1ba8b7fff8800ad9bf143f17d308e02ec68 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 17 Oct 2026 00:18:02 +0900 Subject: [PATCH 82/95] ksmbd: limit sessions on each connection The pending setup limit only counts InProgress sessions. A client can complete authentication repeatedly and retain an unbounded number of Valid sessions on one connection. Reserve a connection slot before allocating a session and retain a connection reference while authentication is in progress. The first SMB3 channel takes over that reservation; binding to another connection reserves one slot there. Reauthentication and authentication continuation reuse the existing reservation. Return slots on allocation or insertion failure, channel removal, and final session destruction. Accept max_sessions_per_connection in reserved startup-request space and default to 64 when older tools leave it zero. Keep the request size and existing field offsets unchanged. Expose the configured limit in procfs. The existing connection limits bound the aggregate number of sessions without a separate global session allocation limit. Fixes: d7fd1f98607f ("ksmbd: follow SMB2 session expiration semantics") Cc: stable@vger.kernel.org Reported-by: Abdifatah Suruur Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 1 + fs/smb/server/connection.h | 2 ++ fs/smb/server/ksmbd_netlink.h | 3 +- fs/smb/server/mgmt/user_session.c | 52 +++++++++++++++++++++++++------ fs/smb/server/mgmt/user_session.h | 6 +++- fs/smb/server/proc.c | 2 ++ fs/smb/server/server.c | 2 ++ fs/smb/server/server.h | 3 ++ fs/smb/server/smb2pdu.c | 16 +++++++++- fs/smb/server/transport_ipc.c | 7 +++++ 10 files changed, 81 insertions(+), 13 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 0a4be9e423a3..625f735a5f07 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -306,6 +306,7 @@ struct ksmbd_conn *ksmbd_conn_alloc(void) atomic_set(&conn->req_running, 0); atomic_set(&conn->r_count, 0); atomic_set(&conn->refcnt, 1); + atomic64_set(&conn->session_count, 0); conn->total_credits = 1; conn->outstanding_credits = 0; diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 53eb91f618b4..e202bf7f3b98 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -80,6 +80,8 @@ struct ksmbd_conn { struct rw_semaphore session_lock; /* smb session 1 per user */ struct xarray sessions; + /* New sessions and SMB3 channels each reserve one connection slot. */ + atomic64_t session_count; unsigned long creation_time; unsigned long last_active; /* How many request are running currently */ diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h index 2673522c76bc..93ebf3f6fbef 100644 --- a/fs/smb/server/ksmbd_netlink.h +++ b/fs/smb/server/ksmbd_netlink.h @@ -114,7 +114,8 @@ struct ksmbd_startup_request { __s8 bind_interfaces_only; __u32 max_ip_connections; /* Number of maximum connection per ip address */ __s8 aapl_model[32]; /* AAPL model string for Finder icon, e.g. "Xserve" */ - __s8 reserved[467]; /* Reserved room */ + __u32 max_sessions_per_connection; /* Maximum sessions per connection */ + __s8 reserved[463]; /* Reserved room */ __u32 ifc_list_sz; /* interfaces list size */ __s8 ____payload[]; } __packed; diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 8314e4f0d534..7c5c0eeedfff 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -3,6 +3,7 @@ * Copyright (C) 2018 Samsung Electronics Co., Ltd. */ +#include #include #include #include @@ -18,6 +19,7 @@ #include "../vfs_cache.h" #include "../misc.h" #include "../stats.h" +#include "../server.h" static DEFINE_IDA(session_ida); @@ -26,6 +28,21 @@ static DEFINE_IDA(session_ida); static DEFINE_HASHTABLE(sessions_table, SESSION_HASH_BITS); static DECLARE_RWSEM(sessions_table_lock); +int ksmbd_session_reserve_slot(struct ksmbd_conn *conn) +{ + if (atomic64_inc_return(&conn->session_count) > + READ_ONCE(server_conf.max_sessions_per_connection)) { + atomic64_dec(&conn->session_count); + return -ENOSPC; + } + return 0; +} + +void ksmbd_session_release_slot(struct ksmbd_conn *conn) +{ + atomic64_dec(&conn->session_count); +} + struct ksmbd_session_rpc { int id; unsigned int method; @@ -271,6 +288,7 @@ static void free_channel_list(struct ksmbd_session *sess) down_write(&sess->chann_lock); xa_for_each(&sess->ksmbd_chann_list, index, chann) { xa_erase(&sess->ksmbd_chann_list, index); + ksmbd_session_release_slot(chann->conn); ksmbd_conn_put(chann->conn); kfree_sensitive(chann); } @@ -398,9 +416,12 @@ int ksmbd_session_rpc_method(struct ksmbd_session *sess, int id) void ksmbd_session_destroy(struct ksmbd_session *sess) { + struct ksmbd_conn *setup_conn; + if (!sess) return; + setup_conn = sess->setup_conn; delete_proc_session(sess); ksmbd_tree_conn_session_logoff(sess); ksmbd_destroy_file_table(sess); @@ -410,9 +431,14 @@ void ksmbd_session_destroy(struct ksmbd_session *sess) ksmbd_session_rpc_clear_list(sess); free_channel_list(sess); kfree_sensitive(sess->Preauth_HashValue); - ksmbd_release_id(&session_ida, sess->id); + if (sess->id) + ksmbd_release_id(&session_ida, sess->id); ida_destroy(&sess->tree_conn_ida); kfree_sensitive(sess); + if (setup_conn) { + ksmbd_session_release_slot(setup_conn); + ksmbd_conn_put(setup_conn); + } } static void ksmbd_session_remove_from_table(struct ksmbd_session *sess) @@ -599,6 +625,7 @@ static int ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess) if (!chann) return -ENOENT; + ksmbd_session_release_slot(chann->conn); ksmbd_conn_put(chann->conn); kfree_sensitive(chann); return 0; @@ -884,7 +911,7 @@ static int __init_smb2_session(struct ksmbd_session *sess) return 0; } -static struct ksmbd_session *__session_create(int protocol) +static struct ksmbd_session *__session_create(struct ksmbd_conn *conn, int protocol) { struct ksmbd_session *sess; int ret; @@ -892,15 +919,17 @@ static struct ksmbd_session *__session_create(int protocol) if (protocol != CIFDS_SESSION_FLAG_SMB2) return NULL; - sess = kzalloc_obj(struct ksmbd_session, KSMBD_DEFAULT_GFP); - if (!sess) + if (ksmbd_session_reserve_slot(conn)) return NULL; + sess = kzalloc_obj(struct ksmbd_session, KSMBD_DEFAULT_GFP); + if (!sess) { + ksmbd_session_release_slot(conn); + return NULL; + } + + sess->setup_conn = ksmbd_conn_get(conn); ida_init(&sess->tree_conn_ida); - - if (ksmbd_init_file_table(&sess->file_table)) - goto error; - sess->last_active = jiffies; sess->state = SMB2_SESSION_IN_PROGRESS; set_session_flag(sess, protocol); @@ -913,6 +942,9 @@ static struct ksmbd_session *__session_create(int protocol) init_rwsem(&sess->rpc_lock); init_rwsem(&sess->chann_lock); + if (ksmbd_init_file_table(&sess->file_table)) + goto error; + ret = __init_smb2_session(sess); if (ret) goto error; @@ -931,9 +963,9 @@ static struct ksmbd_session *__session_create(int protocol) return NULL; } -struct ksmbd_session *ksmbd_smb2_session_create(void) +struct ksmbd_session *ksmbd_smb2_session_create(struct ksmbd_conn *conn) { - return __session_create(CIFDS_SESSION_FLAG_SMB2); + return __session_create(conn, CIFDS_SESSION_FLAG_SMB2); } int ksmbd_acquire_tree_conn_id(struct ksmbd_session *sess) diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h index aff31f1694db..83ddd35c9e95 100644 --- a/fs/smb/server/mgmt/user_session.h +++ b/fs/smb/server/mgmt/user_session.h @@ -37,6 +37,8 @@ struct ksmbd_session { char ClientGUID[SMB2_CLIENT_GUID_SIZE]; struct ksmbd_user *user; + /* Holds the connection slot until an SMB3 channel takes it over. */ + struct ksmbd_conn *setup_conn; unsigned int sequence_number; unsigned int flags; @@ -89,7 +91,9 @@ static inline void clear_session_flag(struct ksmbd_session *sess, int bit) sess->flags &= ~bit; } -struct ksmbd_session *ksmbd_smb2_session_create(void); +struct ksmbd_session *ksmbd_smb2_session_create(struct ksmbd_conn *conn); +int ksmbd_session_reserve_slot(struct ksmbd_conn *conn); +void ksmbd_session_release_slot(struct ksmbd_conn *conn); void ksmbd_session_destroy(struct ksmbd_session *sess); diff --git a/fs/smb/server/proc.c b/fs/smb/server/proc.c index 19f0f2cfbf54..e5a9158c09bd 100644 --- a/fs/smb/server/proc.c +++ b/fs/smb/server/proc.c @@ -161,6 +161,8 @@ static int proc_show_ksmbd_stats(struct seq_file *m, void *v) 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_sessions_per_connection:\t%u\n", + server_conf.max_sessions_per_connection); 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); diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index f9ddbc253595..064f99427ca6 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -474,6 +474,8 @@ static int server_conf_init(void) KSMBD_AUTH_MSKRB5; #endif server_conf.max_inflight_req = SMB2_MAX_CREDITS; + server_conf.max_sessions_per_connection = + KSMBD_DEFAULT_MAX_SESSIONS_PER_CONNECTION; return 0; } diff --git a/fs/smb/server/server.h b/fs/smb/server/server.h index 4d4d268b59d5..7b7df3fafe5b 100644 --- a/fs/smb/server/server.h +++ b/fs/smb/server/server.h @@ -8,6 +8,8 @@ #include "smbacl.h" +#define KSMBD_DEFAULT_MAX_SESSIONS_PER_CONNECTION 64 + /* * Server state type */ @@ -44,6 +46,7 @@ struct ksmbd_server_config { unsigned int max_connections; unsigned int max_inflight_req; unsigned int max_ip_connections; + unsigned int max_sessions_per_connection; char *conf[SERVER_CONF_WORK_GROUP + 1]; struct task_struct *dh_task; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index dbda6dea061e..e49da8c35261 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -113,14 +113,28 @@ static int register_session_channel(struct ksmbd_session *sess, goto out; } + if (sess->setup_conn != conn) { + rc = ksmbd_session_reserve_slot(conn); + if (rc) { + kfree_sensitive(chann); + goto out; + } + } + chann->conn = ksmbd_conn_get(conn); memcpy(chann->sess_key, sess_key, sizeof(chann->sess_key)); old = xa_store(&sess->ksmbd_chann_list, (long)conn, chann, KSMBD_DEFAULT_GFP); if (xa_is_err(old)) { + if (sess->setup_conn != conn) + ksmbd_session_release_slot(conn); ksmbd_conn_put(chann->conn); kfree_sensitive(chann); rc = xa_err(old); + } else if (sess->setup_conn == conn) { + /* Transfer the initial reservation and reference to this channel. */ + sess->setup_conn = NULL; + ksmbd_conn_put(conn); } out: up_write(&sess->chann_lock); @@ -2550,7 +2564,7 @@ int smb2_sess_setup(struct ksmbd_work *work) ksmbd_conn_lock(conn); if (!req->hdr.SessionId) { - sess = ksmbd_smb2_session_create(); + sess = ksmbd_smb2_session_create(conn); if (!sess) { rc = -ENOMEM; goto out_err; diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c index f5cfc320500e..eb07cdf17410 100644 --- a/fs/smb/server/transport_ipc.c +++ b/fs/smb/server/transport_ipc.c @@ -353,6 +353,13 @@ static int ipc_server_config_on_startup(struct ksmbd_startup_request *req) if (req->max_ip_connections) server_conf.max_ip_connections = req->max_ip_connections; + if (req->max_sessions_per_connection) + server_conf.max_sessions_per_connection = + req->max_sessions_per_connection; + else + server_conf.max_sessions_per_connection = + KSMBD_DEFAULT_MAX_SESSIONS_PER_CONNECTION; + ret = ksmbd_set_netbios_name(req->netbios_name); ret |= ksmbd_set_server_string(req->server_string); ret |= ksmbd_set_work_group(req->work_group); From 26534ef335e92b028eb58e44d98d46a0e26af0ec Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 17 Oct 2026 02:06:25 +0900 Subject: [PATCH 83/95] ksmbd: reclaim ended sessions on logoff and expiration LOGOFF and previous-session replacement leave sessions Expired with a zero credential expiry. Ended sessions can retain their memory and connection slots when no further setup arrives on the owning connection. Unpublish a logged-off session while the request reference keeps its response signing and encryption keys alive. Remove its procfs entry before dropping the table reference so a reader cannot remove its own entry on the last put. Clear proc_entry after removal to avoid removing it twice. Reclaim ended sessions from the global table in the existing expiration worker before it acquires conn_list_lock. Only select Expired sessions with zero credential expiry and no references beyond the table reference. Keep sessions whose Kerberos credentials expired available for reauthentication. Unlink the session from its setup connection and all channel connections, so SMB2 sessions and sessions with only secondary SMB3 channels are covered. The session already holds connection references, and sessions_table_lock prevents concurrent channel removal. Queue sessions on a separate disposal list, leaving their hash nodes unhashed, and remove procfs entries and drop table references outside the locks. Keep the pending setup check before allocating a new session so abandoned InProgress sessions can release their slots even at the connection limit. Retain the timeout and reference checks and dispose of those sessions outside the locks. Leave ended Expired sessions to the expiration worker. Fixes: d7fd1f98607f ("ksmbd: follow SMB2 session expiration semantics") Cc: stable@vger.kernel.org Reported-by: Abdifatah Suruur Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 1 + fs/smb/server/mgmt/user_session.c | 94 ++++++++++++++++++++++++------- fs/smb/server/mgmt/user_session.h | 3 + fs/smb/server/smb2pdu.c | 7 +++ 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 625f735a5f07..0fcf90b877ae 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -657,6 +657,7 @@ static void ksmbd_session_expiration_worker(struct work_struct *work) goto reschedule; ksmbd_expire_sessions(); + ksmbd_reclaim_ended_sessions(); /* * An old connection without a Valid or Expired session must be diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 7c5c0eeedfff..db5291a4aa65 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -229,8 +229,10 @@ static int create_proc_session(struct ksmbd_session *sess) static void delete_proc_session(struct ksmbd_session *sess) { - if (sess->proc_entry) + if (sess->proc_entry) { proc_remove(sess->proc_entry); + sess->proc_entry = NULL; + } } static int show_proc_sessions(struct seq_file *m, void *v) @@ -447,6 +449,17 @@ static void ksmbd_session_remove_from_table(struct ksmbd_session *sess) ksmbd_counter_dec(KSMBD_COUNTER_SESSIONS); } +static void ksmbd_dispose_sessions(struct list_head *dispose) +{ + struct ksmbd_session *sess, *tmp; + + list_for_each_entry_safe(sess, tmp, dispose, dispose_entry) { + list_del_init(&sess->dispose_entry); + delete_proc_session(sess); + ksmbd_user_session_put(sess); + } +} + struct ksmbd_session *__session_lookup(unsigned long long id) { struct ksmbd_session *sess; @@ -460,8 +473,9 @@ struct ksmbd_session *__session_lookup(unsigned long long id) return NULL; } -static bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn) +bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn) { + LIST_HEAD(dispose); unsigned long id; struct ksmbd_session *sess; unsigned int pending = 0; @@ -472,18 +486,20 @@ static bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn) if (READ_ONCE(sess->state) != SMB2_SESSION_IN_PROGRESS) continue; - if (atomic_read(&sess->refcnt) <= 1 && + /* Keep response signing keys until every request has dropped its reference. */ + if (atomic_read(&sess->refcnt) == 1 && time_after(jiffies, sess->last_active + KSMBD_UNAUTHENTICATED_CONN_TIMEOUT)) { xa_erase(&conn->sessions, sess->id); ksmbd_session_remove_from_table(sess); - ksmbd_session_destroy(sess); + list_add_tail(&sess->dispose_entry, &dispose); continue; } pending++; } up_write(&conn->session_lock); up_write(&sessions_table_lock); + ksmbd_dispose_sessions(&dispose); return pending >= KSMBD_MAX_PENDING_SESSIONS; } @@ -494,12 +510,8 @@ int ksmbd_session_register(struct ksmbd_conn *conn, sess->dialect = conn->dialect; memcpy(sess->ClientGUID, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); - /* Bound abandoned SessionId-zero authentication exchanges. */ - if (ksmbd_too_many_session_setups(conn)) - ret = -ENOSPC; - else - ret = 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); ksmbd_session_remove_from_table(sess); @@ -510,8 +522,9 @@ int ksmbd_session_register(struct ksmbd_conn *conn, return ret; } -void ksmbd_session_unregister(struct ksmbd_conn *conn, - struct ksmbd_session *sess) +/* The caller holds sessions_table_lock for writing. */ +static bool __ksmbd_session_unregister(struct ksmbd_conn *conn, + struct ksmbd_session *sess) { struct ksmbd_conn *session_conns[KSMBD_MAX_CHANNELS]; struct channel *chann; @@ -519,10 +532,13 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn, unsigned int nr_conns = 0, i; bool removed = false; - down_write(&sessions_table_lock); + lockdep_assert_held_write(&sessions_table_lock); if (!hlist_unhashed(&sess->hlist)) { - /* Keep each channel connection stable under sessions_table_lock. */ + /* Existing references stay valid under sessions_table_lock. */ down_read(&sess->chann_lock); + /* SMB2.0/2.1 sessions have no channel holding their connection. */ + if (sess->setup_conn) + session_conns[nr_conns++] = sess->setup_conn; xa_for_each(&sess->ksmbd_chann_list, index, chann) { if (nr_conns == ARRAY_SIZE(session_conns)) break; @@ -534,10 +550,12 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn, removed = true; } - down_write(&conn->session_lock); - if (xa_load(&conn->sessions, sess->id) == sess) - xa_erase(&conn->sessions, sess->id); - up_write(&conn->session_lock); + if (conn) { + down_write(&conn->session_lock); + if (xa_load(&conn->sessions, sess->id) == sess) + xa_erase(&conn->sessions, sess->id); + up_write(&conn->session_lock); + } for (i = 0; i < nr_conns; i++) { if (session_conns[i] == conn) continue; @@ -546,10 +564,47 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn, xa_erase(&session_conns[i]->sessions, sess->id); up_write(&session_conns[i]->session_lock); } + return removed; +} + +void ksmbd_session_unregister(struct ksmbd_conn *conn, + struct ksmbd_session *sess) +{ + bool removed; + + down_write(&sessions_table_lock); + removed = __ksmbd_session_unregister(conn, sess); up_write(&sessions_table_lock); - if (removed) + if (removed) { + /* + * Keep the table reference until procfs readers have finished, + * so none can remove its own proc entry on the last put. + */ + delete_proc_session(sess); ksmbd_user_session_put(sess); + } +} + +void ksmbd_reclaim_ended_sessions(void) +{ + LIST_HEAD(dispose); + struct ksmbd_session *sess; + struct hlist_node *tmp; + int bkt; + + down_write(&sessions_table_lock); + hash_for_each_safe(sessions_table, bkt, tmp, sess, hlist) { + if (READ_ONCE(sess->state) != SMB2_SESSION_EXPIRED || + READ_ONCE(sess->kerberos_expiry) || + atomic_read(&sess->refcnt) != 1) + continue; + + __ksmbd_session_unregister(NULL, sess); + list_add_tail(&sess->dispose_entry, &dispose); + } + up_write(&sessions_table_lock); + ksmbd_dispose_sessions(&dispose); } bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn) @@ -936,6 +991,7 @@ static struct ksmbd_session *__session_create(struct ksmbd_conn *conn, int proto xa_init(&sess->tree_conns); xa_init(&sess->ksmbd_chann_list); xa_init(&sess->rpc_handle_list); + INIT_LIST_HEAD(&sess->dispose_entry); sess->sequence_number = 1; atomic_set(&sess->refcnt, 2); init_rwsem(&sess->tree_conns_lock); diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h index 83ddd35c9e95..8e4ddb04f0e6 100644 --- a/fs/smb/server/mgmt/user_session.h +++ b/fs/smb/server/mgmt/user_session.h @@ -53,6 +53,7 @@ struct ksmbd_session { u64 kerberos_expiry; struct hlist_node hlist; + struct list_head dispose_entry; struct rw_semaphore chann_lock; struct xarray ksmbd_chann_list; struct xarray tree_conns; @@ -105,6 +106,7 @@ struct ksmbd_session *ksmbd_session_lookup(struct ksmbd_conn *conn, unsigned long long id); bool is_ksmbd_session_in_connection(struct ksmbd_conn *conn, unsigned long long id); +bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn); int ksmbd_session_register(struct ksmbd_conn *conn, struct ksmbd_session *sess); void ksmbd_session_unregister(struct ksmbd_conn *conn, @@ -112,6 +114,7 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn, void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn); bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn); void ksmbd_expire_sessions(void); +void ksmbd_reclaim_ended_sessions(void); struct ksmbd_session *__session_lookup(unsigned long long id); struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, unsigned long long id); diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index e49da8c35261..7b9a39f93068 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2564,6 +2564,11 @@ int smb2_sess_setup(struct ksmbd_work *work) ksmbd_conn_lock(conn); if (!req->hdr.SessionId) { + /* Bound abandoned SessionId-zero authentication exchanges. */ + if (ksmbd_too_many_session_setups(conn)) { + rc = -ENOSPC; + goto out_err; + } sess = ksmbd_smb2_session_create(conn); if (!sess) { rc = -ENOMEM; @@ -3262,6 +3267,8 @@ int smb2_session_logoff(struct ksmbd_work *work) up_write(&conn->session_lock); ksmbd_all_conn_set_status(sess, KSMBD_SESS_NEED_SETUP); + /* Drop the table reference while work->sess pins the response keys. */ + ksmbd_session_unregister(conn, sess); if (err) return err; From 1c087cf7d5386c708d8984b263280b42b3daada0 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 17 Oct 2026 21:43:37 +0900 Subject: [PATCH 84/95] ksmbd: drain session procfs readers before dropping table references Session registration failure and connection cleanup can drop the table reference while the per-session procfs entry is still accessible. If a reader holds the last remaining reference, its final put calls proc_remove() from its own callback and waits for that callback to finish. Remove the procfs entry before dropping the table reference in the registration failure path. Queue sessions removed during connection cleanup and drain their procfs readers after releasing both session locks, then return their table references. Keep the global hash node unhashed while a session is queued so a concurrent unregister cannot drop the reference again. Previous-session replacement also destroys the file table before the session is finally freed. Detach the idr under the lock used by procfs file counters, and report zero files after it has been detached. This prevents a reader from accessing the idr during or after its destruction. Fixes: d7fd1f98607f ("ksmbd: follow SMB2 session expiration semantics") Cc: stable@vger.kernel.org Reported-by: Abdifatah Suruur Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 15 +++++++++------ fs/smb/server/vfs_cache.c | 9 +++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index db5291a4aa65..3ff3671cd019 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -112,8 +112,10 @@ static unsigned int session_open_file_count(struct ksmbd_session *session) unsigned int id; read_lock(&session->file_table.lock); - idr_for_each_entry(session->file_table.idr, fp, id) - count++; + if (session->file_table.idr) { + idr_for_each_entry(session->file_table.idr, fp, id) + count++; + } read_unlock(&session->file_table.lock); return count; } @@ -516,6 +518,7 @@ int ksmbd_session_register(struct ksmbd_conn *conn, down_write(&sessions_table_lock); ksmbd_session_remove_from_table(sess); up_write(&sessions_table_lock); + delete_proc_session(sess); ksmbd_user_session_put(sess); } @@ -688,6 +691,7 @@ static int ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess) void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn) { + LIST_HEAD(dispose); struct ksmbd_session *sess; unsigned long id; struct hlist_node *tmp; @@ -701,8 +705,7 @@ void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn) down_write(&conn->session_lock); xa_erase(&conn->sessions, sess->id); up_write(&conn->session_lock); - if (atomic_dec_and_test(&sess->refcnt)) - ksmbd_session_destroy(sess); + list_add_tail(&sess->dispose_entry, &dispose); } } @@ -712,12 +715,12 @@ void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn) if (xa_empty(&sess->ksmbd_chann_list)) { xa_erase(&conn->sessions, sess->id); ksmbd_session_remove_from_table(sess); - if (atomic_dec_and_test(&sess->refcnt)) - ksmbd_session_destroy(sess); + list_add_tail(&sess->dispose_entry, &dispose); } } up_write(&conn->session_lock); up_write(&sessions_table_lock); + ksmbd_dispose_sessions(&dispose); } bool is_ksmbd_session_in_connection(struct ksmbd_conn *conn, diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index d63fed289c79..35c35638dccc 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -2261,14 +2261,19 @@ int ksmbd_init_file_table(struct ksmbd_file_table *ft) void ksmbd_destroy_file_table(struct ksmbd_session *sess) { struct ksmbd_file_table *ft = &sess->file_table; + struct idr *idr; if (!ft->idr) return; __close_file_table_ids(sess, NULL, session_fd_check, true); - idr_destroy(ft->idr); - kfree(ft->idr); + /* Stop procfs readers before freeing the detached file table. */ + write_lock(&ft->lock); + idr = ft->idr; ft->idr = NULL; + write_unlock(&ft->lock); + idr_destroy(idr); + kfree(idr); } int ksmbd_init_file_cache(void) From df5c868f6b80ca16c171cfac5e4e6c9be83ba4c2 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 20 Oct 2026 10:19:19 +0900 Subject: [PATCH 85/95] ksmbd: fix fd_limit accounting across durable reconnects A disconnected durable handle keeps its ksmbd_file and the fd_limit reservation taken at its initial open. Reconnecting allocates another volatile ID and takes another reservation for the same file. Final close returns only one reservation, so repeated reconnects permanently reduce the server-wide open-file budget. Move reservation into ksmbd_open_fd() and release it on every creation failure. Keep the reservation until final close, including while a durable handle is disconnected. Make ID allocation independent of fd_limit so reconnects neither consume nor return slots. This also avoids returning an unreserved slot when persistent ID allocation fails. New opens continue to return -EMFILE when the budget is exhausted, keeping the existing STATUS_INSUFFICIENT_RESOURCES mapping. Fixes: c8efcc786146 ("ksmbd: add support for durable handles v1/v2") Reported-by: Mohammed Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 35c35638dccc..bfa28ef197f6 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1446,11 +1446,6 @@ static int __open_id(struct ksmbd_file_table *ft, struct ksmbd_file *fp, u64 id = 0; int ret; - if (type == OPEN_ID_TYPE_VOLATILE_ID && fd_limit_depleted()) { - __open_id_set(fp, KSMBD_NO_FID, type); - return -EMFILE; - } - idr_preload(KSMBD_DEFAULT_GFP); write_lock(&ft->lock); ret = idr_alloc_cyclic(ft->idr, fp, KSMBD_START_FID, INT_MAX - 1, @@ -1460,7 +1455,6 @@ static int __open_id(struct ksmbd_file_table *ft, struct ksmbd_file *fp, ret = 0; } else { id = KSMBD_NO_FID; - fd_limit_close(); } __open_id_set(fp, id, type); @@ -1480,10 +1474,15 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) struct ksmbd_file *fp; int ret; + /* Durable reconnects reuse the slot held for the lifetime of fp. */ + if (fd_limit_depleted()) + return ERR_PTR(-EMFILE); + fp = kmem_cache_zalloc(filp_cache, KSMBD_DEFAULT_GFP); if (!fp) { pr_err("Failed to allocate memory\n"); - return ERR_PTR(-ENOMEM); + ret = -ENOMEM; + goto err_fd_limit; } INIT_LIST_HEAD(&fp->blocked_works); @@ -1529,6 +1528,8 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) ksmbd_conn_put(fp->conn); ksmbd_share_config_put(fp->share_conf); kmem_cache_free(filp_cache, fp); +err_fd_limit: + fd_limit_close(); return ERR_PTR(ret); } From da25995071e5e0f3aa9a41160b00b2f833127b3e Mon Sep 17 00:00:00 2001 From: Mohammed Alquraini Date: Tue, 20 Oct 2026 20:30:00 +0300 Subject: [PATCH 86/95] ksmbd: reject foreign RPC pipe handles on IPC$ read and write RPC pipe handles on IPC$ are allocated from a single server-global IDA, and ksmbd.mountd looks a pipe up by handle alone, with no session information. smb2_read_pipe() and smb2_write_pipe() take the handle straight from the client request (VolatileFileId) and pass it to the daemon without checking that the requesting session opened it. An authenticated user can therefore issue an SMB2 READ or WRITE on IPC$ with another session's handle and receive that session's in-flight DCERPC response, or write attacker-chosen bytes into its pipe stream. A tree connect to IPC$, which every RPC client already has, is the only requirement. Validate ownership before the transfer, the way the sibling pipe paths already do: under sess->rpc_lock, ksmbd_session_rpc_method() returns 0 for a handle the session did not open. smb2_get_info_file_pipe() and the FSCTL_PIPE_TRANSCEIVE ioctl already reject such handles; read and write were the remaining gap. Reject them with STATUS_FILE_CLOSED, as the ioctl path does. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Assisted-by: LLM Signed-off-by: Mohammed Alquraini Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 7b9a39f93068..57bdb901a645 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9233,6 +9233,16 @@ static noinline int smb2_read_pipe(struct ksmbd_work *work) id = req->VolatileFileId; + lockdep_assert_not_held(&work->sess->rpc_lock); + down_read(&work->sess->rpc_lock); + if (!ksmbd_session_rpc_method(work->sess, id)) { + up_read(&work->sess->rpc_lock); + rsp->hdr.Status = STATUS_FILE_CLOSED; + smb2_set_err_rsp(work); + return -ENOENT; + } + up_read(&work->sess->rpc_lock); + rpc_resp = ksmbd_rpc_read(work->sess, id); if (rpc_resp) { void *aux_payload_buf; @@ -9863,6 +9873,16 @@ static noinline int smb2_write_pipe(struct ksmbd_work *work) goto out; } + lockdep_assert_not_held(&work->sess->rpc_lock); + down_read(&work->sess->rpc_lock); + if (!ksmbd_session_rpc_method(work->sess, id)) { + up_read(&work->sess->rpc_lock); + rsp->hdr.Status = STATUS_FILE_CLOSED; + smb2_set_err_rsp(work); + return -ENOENT; + } + up_read(&work->sess->rpc_lock); + data_buf = (char *)(((char *)&req->hdr.ProtocolId) + data_offset); From de0cffef897c1c232ed973f89928cd6c93d0b1e3 Mon Sep 17 00:00:00 2001 From: Aamir Ahmed Date: Wed, 16 Sep 2026 11:49:27 +0100 Subject: [PATCH 87/95] ksmbd: deny access for an empty DACL instead of granting the request smb_check_perm_dacl() returns the access mask it computes to the SMB2 CREATE handler, which uses it as the granted access for the open. For a security descriptor whose DACL is present but contains no ACEs, the function is supposed to deny access: an empty (non-NULL) DACL grants no access to anyone except the object owner's implicit READ_CONTROL and WRITE_DAC (MS-DTYP 2.4.5). The empty-DACL branch only denies when both - the DACL has no trailing bytes (pdacl_size == sizeof(struct smb_acl)), and - the request contains bits beyond READ_CONTROL / WRITE_DAC. In every other case it takes the shared "goto err_out" with rc still 0, which returns success without modifying *pdaccess, so the caller grants the full requested access. Three problems follow: - The READ_CONTROL / WRITE_DAC carve-out models the owner's implicit rights, but the check runs before is_owner is computed, so those rights are handed to *every* caller, not just the owner. For READ_CONTROL the SMB2 CREATE path does not fall back to inode_permission(), so any user can read the security descriptor of an object whose empty DACL is meant to deny them. - A request of exactly READ_CONTROL and/or WRITE_DAC is granted to any caller. WRITE_DAC lets the handle rewrite the DACL. - A DACL that declares zero ACEs but carries trailing bytes (pdacl_size > sizeof(struct smb_acl)) skips the deny entirely and is granted whatever was requested. On shares where the NT ACL is the authoritative access control -- e.g. "force user" shares, where every client maps to one POSIX identity so POSIX cannot distinguish users -- this is an access-control bypass. Compute is_owner before the empty-DACL branch and deny access from an empty DACL unless the caller is the owner requesting no more than READ_CONTROL / WRITE_DAC. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Aamir Ahmed Signed-off-by: Namjae Jeon --- fs/smb/server/smbacl.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index c959699ace56..664f01263611 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -1502,21 +1502,27 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, if (pdacl_size > acl_size || pdacl_size < sizeof(struct smb_acl)) goto err_out; - if (!pdacl->num_aces) { - if (!(pdacl_size - sizeof(struct smb_acl)) && - *pdaccess & ~(FILE_READ_CONTROL_LE | FILE_WRITE_DAC_LE)) { - rc = -EACCES; - goto err_out; - } - goto err_out; - } - 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 (!pdacl->num_aces) { + /* + * An empty (present, zero-ACE) DACL grants no access to + * anyone except the object owner's implicit READ_CONTROL + * and WRITE_DAC (MS-DTYP 2.4.5). Deny every other caller, + * deny the owner any access beyond those two bits, and do + * not let trailing bytes after a zero-ACE DACL become an + * implicit grant. + */ + if (!is_owner || + (*pdaccess & ~(FILE_READ_CONTROL_LE | FILE_WRITE_DAC_LE))) + rc = -EACCES; + goto err_out; + } + if (*pdaccess & FILE_MAXIMAL_ACCESS_LE) { ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); aces_size = pdacl_size - sizeof(struct smb_acl); From 8323aab48525b83f750e712f66e6e9387b550cf4 Mon Sep 17 00:00:00 2001 From: Michail Andreas Michalopoulos Date: Tue, 20 Oct 2026 21:03:51 +0200 Subject: [PATCH 88/95] ksmbd: reject FILE_ATTRIBUTE_TEMPORARY on directories Setting FILE_ATTRIBUTE_TEMPORARY on a directory with SMB2 SET_INFO (FileBasicInformation) succeeds: ksmbd keeps the attribute and, with "store dos attributes" enabled, also writes it to the DOS attribute xattr. MS-FSA (FileBasicInformation) says the request must fail with STATUS_INVALID_PARAMETER when FILE_ATTRIBUTE_TEMPORARY is set on a directory, and Samba rejects it too. smbtorture's smb2.create.dosattr_tmp_dir checks this and fails against ksmbd. Return -EINVAL, which is sent as STATUS_INVALID_PARAMETER, when a directory is asked to become TEMPORARY. Do the check before any timestamp is changed, so a rejected request leaves the open file and the inode unchanged. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Assisted-by: LLM Signed-off-by: Michail Andreas Michalopoulos 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 57bdb901a645..45336491b576 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8695,6 +8695,11 @@ static int set_file_basic_info(struct ksmbd_file *fp, inode = file_inode(filp); idmap = file_mnt_idmap(filp); + /* MS-FSA FileBasicInformation: a directory can't be marked TEMPORARY */ + if (S_ISDIR(inode->i_mode) && + (file_info->Attributes & FILE_ATTRIBUTE_TEMPORARY_LE)) + return -EINVAL; + if (file_info->CreationTime) fp->create_time = le64_to_cpu(file_info->CreationTime); From f75c932786083b8b16ecff714a0f2f0fa9323413 Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Wed, 21 Oct 2026 15:02:57 +0800 Subject: [PATCH 89/95] smb/server: replace deprecated system workqueues Queueing the lease break timer on system_wq triggers the following warning: workqueue: work func lease_break_timer [ksmbd] enqueued on deprecated workqueue. Use system_{percpu|dfl}_wq instead. The oplock break timer and TCP transmit work also use deprecated system queues. Signed-off-by: ZhangGuoDong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 4 ++-- fs/smb/server/transport_tcp.c | 6 +++--- fs/smb/server/vfs_cache.c | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index e76dfc904fb9..d479508c06c5 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -178,7 +178,7 @@ static void lease_break_timer(struct work_struct *work) lease_break_timer_pending = pending; if (pending) { lease_break_timer_next = next; - mod_delayed_work(system_wq, &lease_break_timer_work, + mod_delayed_work(system_percpu_wq, &lease_break_timer_work, time_after_eq(jiffies, next) ? 0 : next - jiffies); } mutex_unlock(&lease_break_timer_lock); @@ -191,7 +191,7 @@ static void schedule_lease_break_timer(unsigned long timeout) time_before(timeout, lease_break_timer_next)) { lease_break_timer_pending = true; lease_break_timer_next = timeout; - mod_delayed_work(system_wq, &lease_break_timer_work, + mod_delayed_work(system_percpu_wq, &lease_break_timer_work, time_after_eq(jiffies, timeout) ? 0 : timeout - jiffies); } mutex_unlock(&lease_break_timer_lock); diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 9c6ca878d30f..937d321fa364 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -573,7 +573,7 @@ static void ksmbd_tcp_tx_work(struct work_struct *work) if (READ_ONCE(t->tx_stopped)) ksmbd_tcp_purge_tx(t); else if (!list_empty(&t->tx_queue)) - queue_delayed_work(system_unbound_wq, &t->tx_work, delay); + queue_delayed_work(system_dfl_wq, &t->tx_work, delay); mutex_unlock(&t->tx_lock); wake_up_all(&t->transport.conn->req_running_q); @@ -590,7 +590,7 @@ static void ksmbd_tcp_write_space(struct sock *sk) if (t) { t->write_space(sk); if (!READ_ONCE(t->tx_stopped) && sk_stream_is_writeable(sk)) - mod_delayed_work(system_unbound_wq, &t->tx_work, 0); + mod_delayed_work(system_dfl_wq, &t->tx_work, 0); } read_unlock_bh(&sk->sk_callback_lock); } @@ -687,7 +687,7 @@ static int ksmbd_tcp_writev(struct ksmbd_transport *t, msg->deadline = deadline; list_add_tail(&msg->list, &tt->tx_queue); WRITE_ONCE(tt->tx_queued, tt->tx_queued + msg->len); - queue_delayed_work(system_unbound_wq, &tt->tx_work, 0); + queue_delayed_work(system_dfl_wq, &tt->tx_work, 0); accepted: tt->tx_end += tx->size; diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index bfa28ef197f6..452ce05d1494 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -87,7 +87,7 @@ static void ksmbd_oplock_break_timer(struct work_struct *work) oplock_break_timer_pending = pending; if (pending) { oplock_break_timer_next = next; - mod_delayed_work(system_wq, &oplock_break_timer_work, + mod_delayed_work(system_percpu_wq, &oplock_break_timer_work, time_after_eq(jiffies, next) ? 0 : next - jiffies); } mutex_unlock(&oplock_break_timer_lock); @@ -100,7 +100,7 @@ void ksmbd_schedule_oplock_break_timer(unsigned long timeout) time_before(timeout, oplock_break_timer_next)) { oplock_break_timer_pending = true; oplock_break_timer_next = timeout; - mod_delayed_work(system_wq, &oplock_break_timer_work, + mod_delayed_work(system_percpu_wq, &oplock_break_timer_work, time_after_eq(jiffies, timeout) ? 0 : timeout - jiffies); } mutex_unlock(&oplock_break_timer_lock); From 0fec4c24e1979032d7b7c159d9e5c45c7444e0f2 Mon Sep 17 00:00:00 2001 From: Michail Andreas Michalopoulos Date: Wed, 21 Oct 2026 13:54:00 +0200 Subject: [PATCH 90/95] ksmbd: restore Kerberos logins with released ksmbd-tools Commit a47634cd729b ("ksmbd: expire SMB sessions when Kerberos tickets expire") added a __u64 session_expiry field to struct ksmbd_spnego_authen_response, growing it from 156 to 168 bytes. ipc_validate_msg() requires the response to have exactly that size, so the 156-byte response of every released ksmbd-tools, up to 3.5.7, is dropped and Kerberos session setup fails with STATUS_LOGON_FAILURE. The other way round, kernels without that commit, including the stable trees, drop the 168-byte response of current ksmbd-tools master, and ksmbd-tools has no way to tell which layout a kernel expects. Pass the expiry in the reserved area of the embedded struct ksmbd_login_response instead. ksmbd-tools releases with that area (3.4.4 and later) allocate the response zeroed and never write to it, so they send an expiry of 0, which ksmbd already treats as no expiry. The response is 156 bytes again, so it works with those releases, and every kernel also accepts a response that sets the expiry. Use two __u32 halves, as the reserved area is only 4-byte aligned. Fixes: a47634cd729b ("ksmbd: expire SMB sessions when Kerberos tickets expire") Cc: stable@vger.kernel.org Closes: https://lore.kernel.org/all/CAO+rqLhpPvY0NLrAnZjFXz0gOXnA6zsXxK8xCgtA42XUkSsAeg@mail.gmail.com/ Assisted-by: LLM Signed-off-by: Michail Andreas Michalopoulos Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 3 ++- fs/smb/server/ksmbd_netlink.h | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index 88953315b160..0c51ca0cb14d 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -463,7 +463,8 @@ 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; + sess->kerberos_expiry = (u64)resp->login_response.session_expiry_hi << 32 | + resp->login_response.session_expiry_lo; retval = 0; out: kvfree_sensitive(resp, sizeof(*resp) + resp->session_key_len + diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h index 93ebf3f6fbef..ad050988659e 100644 --- a/fs/smb/server/ksmbd_netlink.h +++ b/fs/smb/server/ksmbd_netlink.h @@ -149,7 +149,9 @@ struct ksmbd_login_response { __u16 status; __u16 hash_sz; /* hash size */ __s8 hash[KSMBD_REQ_MAX_HASH_SZ]; /* password hash */ - __u32 reserved[16]; /* Reserved room */ + __u32 session_expiry_lo; /* Kerberos ticket expiry, SPNEGO only */ + __u32 session_expiry_hi; + __u32 reserved[14]; /* Reserved room */ }; /* @@ -287,7 +289,6 @@ 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 */ }; From 2ff3730998b015b5b0a3f28330bee16c648dd114 Mon Sep 17 00:00:00 2001 From: Weiming Shi Date: Wed, 21 Oct 2026 07:17:57 -0700 Subject: [PATCH 91/95] smb/server: prevent file publication after tree disconnect TREE_DISCONNECT closes the tree's published file handles before marking and removing the tree connection. A concurrent CREATE that already holds a tree reference can therefore publish its file after the close pass. When CREATE drops its reference, the tree can be freed while the published file still points to it. Mark and remove the tree connection before closing its files, and serialize both new-open and durable-reconnect volatile ID publication with that state transition. An opener is then either published in time for the close pass or rejected after disconnect with STATUS_NETWORK_NAME_DELETED. Keep a tree reference while a file carries its tree pointer. This protects asynchronous users that can defer the file's final destruction past tree disconnect. The close pass can race the successful FP_NEW to FP_INITED transition. Take the opening request's file reference atomically with that transition. This keeps the file alive until request completion even if teardown removes the ID immediately. BUG: KASAN: slab-use-after-free in proc_show_files (fs/smb/server/vfs_cache.c:126) Read of size 4 at addr ff11000101540f80 by task poc/153 Call Trace: proc_show_files (fs/smb/server/vfs_cache.c:126) seq_read_iter (fs/seq_file.c:231) proc_reg_read_iter (fs/proc/inode.c:299) vfs_read (fs/read_write.c:574) ksys_read (fs/read_write.c:716) do_syscall_64 (arch/x86/entry/syscall_64.c:84) entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121) Allocated by: ksmbd_tree_conn_connect (fs/smb/server/mgmt/tree_connect.c:35) smb2_tree_connect (fs/smb/server/smb2pdu.c:2819) handle_ksmbd_work (fs/smb/server/server.c:368) Freed by: kfree (mm/slub.c:6801) handle_ksmbd_work (fs/smb/server/server.c:332) Fixes: f44158485826 ("cifsd: add file operations") Cc: stable@vger.kernel.org Reported-by: Assisted-by: LLM Signed-off-by: Weiming Shi Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/tree_connect.c | 7 ++++ fs/smb/server/mgmt/tree_connect.h | 2 ++ fs/smb/server/smb2pdu.c | 37 ++++++++++++++----- fs/smb/server/vfs_cache.c | 60 +++++++++++++++++++++---------- 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/fs/smb/server/mgmt/tree_connect.c b/fs/smb/server/mgmt/tree_connect.c index dd1db3554cae..837175ac5ce8 100644 --- a/fs/smb/server/mgmt/tree_connect.c +++ b/fs/smb/server/mgmt/tree_connect.c @@ -103,6 +103,13 @@ ksmbd_tree_conn_connect(struct ksmbd_work *work, const char *share_name) return status; } +struct ksmbd_tree_connect * +ksmbd_tree_connect_get(struct ksmbd_tree_connect *tcon) +{ + atomic_inc(&tcon->refcount); + return tcon; +} + void ksmbd_tree_connect_put(struct ksmbd_tree_connect *tcon) { if (atomic_dec_and_test(&tcon->refcount)) { diff --git a/fs/smb/server/mgmt/tree_connect.h b/fs/smb/server/mgmt/tree_connect.h index f0023d86716f..82891ff9b7b9 100644 --- a/fs/smb/server/mgmt/tree_connect.h +++ b/fs/smb/server/mgmt/tree_connect.h @@ -51,6 +51,8 @@ struct ksmbd_session; struct ksmbd_tree_conn_status ksmbd_tree_conn_connect(struct ksmbd_work *work, const char *share_name); +struct ksmbd_tree_connect * +ksmbd_tree_connect_get(struct ksmbd_tree_connect *tcon); void ksmbd_tree_connect_put(struct ksmbd_tree_connect *tcon); int ksmbd_tree_conn_disconnect(struct ksmbd_session *sess, diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 45336491b576..da82d80c222a 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3175,9 +3175,10 @@ int smb2_tree_disconnect(struct ksmbd_work *work) goto err_out; } + err = ksmbd_tree_conn_disconnect(sess, tcon); + ksmbd_close_tree_conn_fds(work); - err = ksmbd_tree_conn_disconnect(sess, tcon); if (err) { rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED; goto err_out; @@ -4502,8 +4503,12 @@ int smb2_open(struct ksmbd_work *work) goto err_out2; rc = ksmbd_reopen_durable_fd(work, dh_info.fp); - if (rc) + if (rc) { + if (rc == -ENOTCONN) + rsp->hdr.Status = + STATUS_NETWORK_NAME_DELETED; goto err_out2; + } fp = dh_info.fp; @@ -4940,6 +4945,8 @@ int smb2_open(struct ksmbd_work *work) if (IS_ERR(fp)) { fput(filp); rc = PTR_ERR(fp); + if (rc == -ENOTCONN) + rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED; fp = NULL; goto err_out; } @@ -5544,11 +5551,20 @@ int smb2_open(struct ksmbd_work *work) err_out2: if (!rc) { - if (!dh_info.replay) + if (!dh_info.replay) { + smb2_complete_request_open(work); rc = ksmbd_update_fstate(&work->sess->file_table, fp, FP_INITED); - if (!rc) + if (rc == -ENOENT) + rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED; + if (!rc) { + work->request_open = fp; + work->request_open_chseq = + smb3_hdr_channel_sequence(&req->hdr); + } + } else { rc = smb2_set_request_open(work, fp, &req->hdr, false, false); + } if (!rc) rc = ksmbd_iov_pin_rsp(work, (void *)rsp, iov_len); } @@ -5563,8 +5579,7 @@ int smb2_open(struct ksmbd_work *work) rsp->hdr.Status = STATUS_PRIVILEGE_NOT_HELD; else rsp->hdr.Status = STATUS_ACCESS_DENIED; - } - else if (rc == -ENOENT) + } else if (rc == -ENOENT && !rsp->hdr.Status) rsp->hdr.Status = STATUS_OBJECT_NAME_INVALID; else if (rc == -EPERM) rsp->hdr.Status = STATUS_SHARING_VIOLATION; @@ -5587,8 +5602,14 @@ int smb2_open(struct ksmbd_work *work) if (!rsp->hdr.Status) rsp->hdr.Status = STATUS_UNEXPECTED_IO_ERROR; - if (fp && !dh_info.replay) - ksmbd_fd_put(work, fp); + if (fp && !dh_info.replay) { + if (work->request_open == fp) { + ksmbd_close_fd(work, fp->volatile_id); + smb2_complete_request_open(work); + } else { + ksmbd_fd_put(work, fp); + } + } smb2_set_err_rsp(work); ksmbd_debug(SMB, "Error response: %x\n", rsp->hdr.Status); } diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 452ce05d1494..7a0f13277ec0 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -919,16 +919,18 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) fput(filp); /* - * Drop fp's strong reference on conn (taken in ksmbd_open_fd() / - * ksmbd_reopen_durable_fd()). Durable fps that reached the - * scavenger have already had fp->conn cleared by session_fd_check(), - * in which case there is nothing to drop here. + * Drop fp's strong connection references. Durable fps that reached + * the scavenger have already had these cleared by session_fd_check(). */ if (fp->conn) { atomic_dec(&fp->conn->stats.open_files_count); ksmbd_conn_put(fp->conn); fp->conn = NULL; } + if (fp->tcon) { + ksmbd_tree_connect_put(fp->tcon); + fp->tcon = NULL; + } if (ksmbd_stream_fd(fp)) kfree(fp->stream.name); @@ -1494,14 +1496,13 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) fp->filp = filp; /* - * fp owns a strong reference on fp->conn for as long as fp->conn is - * non-NULL, so session_fd_check() and __ksmbd_close_fd() never - * dereference a dangling pointer. Paired with ksmbd_conn_put() in - * session_fd_check() (durable preserve), in __ksmbd_close_fd() - * (final close), and on the error paths below. + * fp owns strong references on fp->conn and fp->tcon for as long as + * they are non-NULL. Paired with the puts in session_fd_check() + * (durable preserve), in __ksmbd_close_fd() (final close), and on the + * error paths below. */ fp->conn = ksmbd_conn_get(work->conn); - fp->tcon = work->tcon; + fp->tcon = ksmbd_tree_connect_get(work->tcon); fp->share_conf = work->tcon->share_conf; atomic_inc(&fp->share_conf->refcount); fp->volatile_id = KSMBD_NO_FID; @@ -1514,7 +1515,13 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) goto err_out; } - ret = __open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID); + down_read(&work->sess->tree_conns_lock); + if (work->tcon->t_state == TREE_CONNECTED) + ret = __open_id(&work->sess->file_table, fp, + OPEN_ID_TYPE_VOLATILE_ID); + else + ret = -ENOTCONN; + up_read(&work->sess->tree_conns_lock); if (ret) { ksmbd_inode_put(fp->f_ci); goto err_out; @@ -1524,7 +1531,8 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) return fp; err_out: - /* fp->conn was set and refcounted before every branch here. */ + /* fp->conn and fp->tcon were refcounted before every branch here. */ + ksmbd_tree_connect_put(fp->tcon); ksmbd_conn_put(fp->conn); ksmbd_share_config_put(fp->share_conf); kmem_cache_free(filp_cache, fp); @@ -1541,8 +1549,9 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) * * Return: 0 on success. The FP_NEW -> FP_INITED transition is special: * -ENOENT if teardown already unpublished @fp by advancing the state or - * clearing the volatile id. Other state updates preserve the historical - * fire-and-forget behavior. + * clearing the volatile id. On success it also takes a reference for the + * opening request, which must be released with ksmbd_fd_put(). Other state + * updates preserve the historical fire-and-forget behavior. */ int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp, unsigned int state) @@ -1558,6 +1567,8 @@ int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp, ret = -ENOENT; } else { fp->f_state = state; + if (state == FP_INITED) + atomic_inc(&fp->refcount); ret = 0; } write_unlock(&ft->lock); @@ -1977,6 +1988,7 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, struct ksmbd_inode *ci; struct oplock_info *op; struct ksmbd_conn *conn; + struct ksmbd_tree_connect *tree_conn; if (!is_reconnectable(fp)) return false; @@ -1998,6 +2010,7 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, ci = fp->f_ci; down_write(&ci->m_lock); conn = fp->conn; + tree_conn = fp->tcon; list_for_each_entry_rcu(op, &ci->m_op_list, op_entry, lockdep_is_held(&ci->m_lock)) { if (op->o_fp != fp) @@ -2021,6 +2034,7 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, /* Drop fp's own reference on conn. */ ksmbd_conn_put(conn); + ksmbd_tree_connect_put(tree_conn); return true; } @@ -2133,7 +2147,8 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) * concurrent teardown that iterates the table can observe a valid * volatile_id with fp->conn == NULL and preserve a * partially-initialized fp. fp owns a strong reference on the new - * conn (see ksmbd_open_fd()); undo it on __open_id() failure. + * conn (see ksmbd_open_fd()); undo it on __open_id() failure. Serialize + * publication with tree disconnect just as ksmbd_open_fd() does. */ spin_lock(&fp->f_lock); old_f_state = fp->f_state; @@ -2142,13 +2157,19 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) fp->share_conf = work->tcon->share_conf; atomic_inc(&fp->share_conf->refcount); fp->conn = ksmbd_conn_get(conn); - fp->tcon = work->tcon; + fp->tcon = ksmbd_tree_connect_get(work->tcon); spin_unlock(&fp->f_lock); atomic_inc(&conn->stats.open_files_count); write_unlock(&global_ft.lock); - __open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID); - if (!has_file_id(fp->volatile_id)) { + down_read(&work->sess->tree_conns_lock); + if (work->tcon->t_state == TREE_CONNECTED) + ret = __open_id(&work->sess->file_table, fp, + OPEN_ID_TYPE_VOLATILE_ID); + else + ret = -ENOTCONN; + up_read(&work->sess->tree_conns_lock); + if (ret) { write_lock(&global_ft.lock); spin_lock(&fp->f_lock); fp->conn = NULL; @@ -2159,7 +2180,10 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) atomic_dec(&conn->stats.open_files_count); write_unlock(&global_ft.lock); ksmbd_conn_put(conn); + ksmbd_tree_connect_put(work->tcon); ksmbd_share_config_put(work->tcon->share_conf); + if (ret != -ENOTCONN) + ret = -EBADF; goto out; } From 171df27077fb49b4af2c53ad2cedcd0e39664fe8 Mon Sep 17 00:00:00 2001 From: "Cen Zhang (Microsoft)" Date: Thu, 22 Oct 2026 00:12:16 -0400 Subject: [PATCH 92/95] ksmbd: fix parent dentry lifetime in directory enumeration ksmbd_populate_dot_dotdot_entries() builds the ".." entry from the open directory's d_parent and passes that pointer to ksmbd_vfs_fill_dentry_attrs() without taking a reference. That pointer is only valid while the directory stays under this parent. If a concurrent rename moves the directory elsewhere, the old parent loses its last reference; a following rmdir then turns it into a negative dentry with d_inode cleared, and after the RCU grace period the dentry itself is freed. The worker still holds the raw pointer and dereferences d_inode through vfs_getattr() and vfs_getxattr(). A remote SMB client, authenticated or guest, triggers this by listing a directory while that directory is moved under a new parent and the old parent is deleted. It can be done by the client itself with write access or by other concurrent writers, triggering null-ptr-deref and slab-use-after-free on a KASAN build. KASAN: null-ptr-deref in range [0x0000000000000000-0x0000000000000007] Workqueue: ksmbd-io handle_ksmbd_work RIP: 0010:xattr_permission+0x9d/0x160 Call Trace: vfs_getxattr+0x9c/0x1b0 ksmbd_vfs_getxattr+0x82/0x100 ksmbd_vfs_get_dos_attrib_xattr+0x7f/0x130 ksmbd_vfs_fill_dentry_attrs+0x435/0x650 ksmbd_populate_dot_dotdot_entries+0x25e/0x630 smb2_query_dir+0xcf9/0x1a20 handle_ksmbd_work+0x5f1/0x1c80 process_one_work+0x90b/0x19d0 Fix by using the dentry reference counting APIs: take the parent with dget_parent(), take a matching dget() on ".", and dput() the dentry once ksmbd_vfs_fill_dentry_attrs() has returned or the pattern does not match. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Cc: AutonomousCodeSecurity@microsoft.com Signed-off-by: Cen Zhang (Microsoft) Signed-off-by: Namjae Jeon --- fs/smb/server/smb_common.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index 7dbcfa658edd..f9fb3741ada3 100644 --- a/fs/smb/server/smb_common.c +++ b/fs/smb/server/smb_common.c @@ -478,16 +478,17 @@ int ksmbd_populate_dot_dotdot_entries(struct ksmbd_work *work, int info_level, if (i == 0) { d_info->name = "."; d_info->name_len = 1; - dentry = dir->filp->f_path.dentry; + dentry = dget(dir->filp->f_path.dentry); } else { d_info->name = ".."; d_info->name_len = 2; - dentry = dir->filp->f_path.dentry->d_parent; + dentry = dget_parent(dir->filp->f_path.dentry); } if (!match_pattern(d_info->name, d_info->name_len, search_pattern)) { dir->dot_dotdot[i] = 1; + dput(dentry); continue; } @@ -496,6 +497,7 @@ int ksmbd_populate_dot_dotdot_entries(struct ksmbd_work *work, int info_level, idmap, dentry, &ksmbd_kstat); + dput(dentry); if (rc) break; From 7220980b2423d06a0604516129bfa4cec8a697ae Mon Sep 17 00:00:00 2001 From: Yongjun Gwon Date: Fri, 23 Oct 2026 02:38:32 +0900 Subject: [PATCH 93/95] ksmbd: serialize directory emptiness checks with readdir SMB2 QUERY_DIRECTORY holds fp->readdir_lock while it calls iterate_dir() on fp->filp. An SMB2 SET_INFO request with FILE_DISPOSITION_INFORMATION can resolve the same FileId and call ksmbd_vfs_empty_dir() on the same filp without that lock. iterate_dir()'s inode read lock permits concurrent readers and does not serialize per-file iterator state. ext4 stores its dx readdir cursor and fname RB tree in file->private_data. Concurrent reset or refill can therefore free a fname node while the other worker still traverses it or retains its RB-tree link, resulting in use-after-free accesses. An authenticated SMB client with list and delete rights on a directory in a writable KSMBD share backed by ext4 can trigger this when the directory takes the dx readdir path. The client overlaps an SMB2 QUERY_DIRECTORY request with an SMB2 SET_INFO request carrying FILE_DISPOSITION_INFORMATION on one live FileId. In one x86_64 SMP KASAN multi-shot guest using ksmbd-for-next at 171df27077fb, the unmodified kernel reported a cross-worker use-after-free before a panic. The first report's access and free stacks show the two request paths directly (trimmed): BUG: KASAN: slab-use-after-free in ext4_htree_store_dirent+0x488/0x550 Read of size 8 by task kworker/2:0/29 Workqueue: ksmbd-io handle_ksmbd_work Call Trace: ext4_htree_store_dirent+0x488/0x550 htree_dirblock_to_tree+0x535/0x990 ext4_htree_fill_tree+0x319/0xba0 ext4_readdir+0x175a/0x2f40 iterate_dir+0x1b6/0x630 smb2_query_dir+0x1003/0x1cc0 Freed by task 279: free_rb_tree_fname+0x67/0xc0 ext4_readdir+0x13a9/0x2f40 iterate_dir+0x1b6/0x630 ksmbd_vfs_empty_dir+0xa2/0x100 smb2_set_info+0x2ffa/0x45d0 Take fp->readdir_lock across the entire emptiness check. This places both iterators over that handle's filp in the same lock domain while preserving the disposition status handling. The exact code change completed a warning-free x86_64 KASAN bzImage build. Tracing three request pairs showed that the two iterate_dir() calls no longer overlapped and the expected statuses were preserved. A further 1,600 reproducer iterations completed with expected statuses and no KASAN report, Oops, or panic. The reproducer and complete guest configuration are available privately to maintainers on request. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Yongjun Gwon Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index da82d80c222a..f5fe3cf8ba32 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8918,6 +8918,7 @@ static int set_file_disposition_info(struct ksmbd_work *work, struct smb2_file_disposition_info *file_info) { struct inode *inode; + int err; if (!(fp->daccess & FILE_DELETE_LE)) { pr_err("no right to delete : 0x%x\n", fp->daccess); @@ -8932,9 +8933,13 @@ 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) && !ksmbd_stream_fd(fp) && - ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY) - return -EBUSY; + if (S_ISDIR(inode->i_mode) && !ksmbd_stream_fd(fp)) { + mutex_lock(&fp->readdir_lock); + err = ksmbd_vfs_empty_dir(fp); + mutex_unlock(&fp->readdir_lock); + if (err == -ENOTEMPTY) + return -EBUSY; + } smb_break_all_levII_oplock_for_delete(work, fp); ksmbd_fd_set_delete_pending(fp); } else { From 9fd1fcbbf84986a7bcf601a2b90540899069d263 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 23 Oct 2026 08:53:57 +0900 Subject: [PATCH 94/95] ksmbd: prevent share root removal through empty paths A FileLinkInformation request can supply "/" as the new link name. smb2_get_name() strips the trailing slash and passes an empty string to ksmbd_vfs_kern_path_start_removing(). ksmbd_vfs_path_lookup() resolves an empty pathname as the configured share path, without LOOKUP_BENEATH. With ReplaceIfExists set, smb2_create_link() can therefore remove the share root from its parent before link creation fails. An authenticated client with a writable share and DELETE access to a file can trigger this if the share root is empty and the mapped user has write and search permission on its parent directory. Return -EINVAL for empty pathnames when for_remove is set, before resolving the share path or acquiring removal resources. Ordinary lookups continue to accept empty pathnames for opening the share root. Fixes: 74d7970febf7 ("ksmbd: fix racy issue from using ->d_parent and ->d_name") Cc: stable@vger.kernel.org Reported-by: Zachariah Mithani (AploSec) Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 5360c6999bb3..4ee72eda9bc0 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -62,6 +62,10 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf, struct dentry *d; if (pathname[0] == '\0') { + /* The share root must never be removed. */ + if (for_remove) + return -EINVAL; + pathname = share_conf->path; root_share_path = NULL; } else { From 2c36c4a86104d763ee798d4a89e2d3384b4cd626 Mon Sep 17 00:00:00 2001 From: Michail Andreas Michalopoulos Date: Fri, 23 Oct 2026 02:40:45 +0200 Subject: [PATCH 95/95] ksmbd: select CRYPTO_AES ksmbd allocates "gcm(aes)" and "ccm(aes)" AEAD transforms for SMB3 encryption, but SMB_SERVER only selects CRYPTO_GCM and CRYPTO_CCM, not an AES cipher. With a configuration in which nothing else enables CRYPTO_AES, ksmbd builds but cannot allocate the transforms, and every session that uses encryption fails: ksmbd: Failed to alloc encrypt aead : -2 ksmbd: crypto alloc failed Select CRYPTO_AES, as the SMB client already does. Fixes: a848c4f15ab6 ("cifsd: add Kconfig and Makefile") Assisted-by: LLM Signed-off-by: Michail Andreas Michalopoulos Signed-off-by: Namjae Jeon --- fs/smb/server/Kconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/smb/server/Kconfig b/fs/smb/server/Kconfig index b7665e0e4942..f8b03605db59 100644 --- a/fs/smb/server/Kconfig +++ b/fs/smb/server/Kconfig @@ -16,6 +16,7 @@ config SMB_SERVER select CRYPTO_AEAD2 select CRYPTO_CCM select CRYPTO_GCM + select CRYPTO_AES select ASN1 select OID_REGISTRY select CRC32