From c1016dd1d8b2bcd1158bbaabe94a31bb7e7431fb Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 15 Jun 2026 10:00:00 +0900 Subject: [PATCH 01/50] ksmbd: track the connection owning a byte-range lock SMB2_LOCK adds each granted byte-range lock to both the file lock list and the lock list of the connection which handled the request. The final close and durable handle paths, however, remove the connection list entry while holding fp->conn->llist_lock. With SMB3 multichannel, the connection handling the LOCK request can be different from the connection which opened the file. The entry can therefore be removed under a different spinlock from the one protecting the list it belongs to. A concurrent traversal can then access freed struct ksmbd_lock and struct file_lock objects. Record the connection owning each lock's clist entry and hold a reference to it while the entry is linked. Use that connection and its llist_lock for unlock, rollback, close, and durable preserve. Durable reconnect assigns the new connection as the owner when publishing the locks again. Fixes: f5a544e3bab7 ("ksmbd: add support for SMB3 multichannel") Cc: stable@vger.kernel.org Reported-by: Musaab Khan Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 10 ++++++++-- fs/smb/server/vfs_cache.c | 23 +++++++++++++++++------ fs/smb/server/vfs_cache.h | 1 + 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index c1c6c1a64f60..ba24040d05eb 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7774,9 +7774,11 @@ int smb2_lock(struct ksmbd_work *work) nolock = 0; list_del(&cmp_lock->flist); list_del(&cmp_lock->clist); + cmp_lock->conn = NULL; spin_unlock(&conn->llist_lock); up_read(&conn_list_lock); + ksmbd_conn_put(conn); locks_free_lock(cmp_lock->fl); kfree(cmp_lock); goto out_check_cl; @@ -7911,6 +7913,7 @@ int smb2_lock(struct ksmbd_work *work) goto out2; } else if (!rc) { list_add(&smb_lock->llist, &rollback_list); + smb_lock->conn = ksmbd_conn_get(work->conn); spin_lock(&work->conn->llist_lock); list_add_tail(&smb_lock->clist, &work->conn->lock_list); @@ -7965,11 +7968,14 @@ int smb2_lock(struct ksmbd_work *work) } list_del(&smb_lock->llist); - spin_lock(&work->conn->llist_lock); + conn = smb_lock->conn; + spin_lock(&conn->llist_lock); if (!list_empty(&smb_lock->flist)) list_del(&smb_lock->flist); list_del(&smb_lock->clist); - spin_unlock(&work->conn->llist_lock); + smb_lock->conn = NULL; + spin_unlock(&conn->llist_lock); + ksmbd_conn_put(conn); locks_free_lock(smb_lock->fl); if (rlock) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 8c556e46cc10..39c56942ae44 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -484,10 +484,14 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) * there are not accesses to fp->lock_list. */ list_for_each_entry_safe(smb_lock, tmp_lock, &fp->lock_list, flist) { - if (!list_empty(&smb_lock->clist) && fp->conn) { - spin_lock(&fp->conn->llist_lock); - list_del(&smb_lock->clist); - spin_unlock(&fp->conn->llist_lock); + 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(&smb_lock->flist); @@ -1303,9 +1307,15 @@ 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) { - spin_lock(&conn->llist_lock); + struct ksmbd_conn *lock_conn = smb_lock->conn; + + if (!lock_conn) + continue; + spin_lock(&lock_conn->llist_lock); list_del_init(&smb_lock->clist); - spin_unlock(&conn->llist_lock); + smb_lock->conn = NULL; + spin_unlock(&lock_conn->llist_lock); + ksmbd_conn_put(lock_conn); } fp->conn = NULL; @@ -1435,6 +1445,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) } 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); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 7d547e1a74f7..a3a9fda6de91 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -32,6 +32,7 @@ struct ksmbd_session; struct ksmbd_lock { struct file_lock *fl; + struct ksmbd_conn *conn; struct list_head clist; struct list_head flist; struct list_head llist; From 37ee476071bcbf4fa97a5a7b208b3a51073b4fcc Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Wed, 17 Jun 2026 02:36:32 +0000 Subject: [PATCH 02/50] smb/server: fix debug log endianness in smb2_cancel() Convert to CPU byte order to avoid incorrect debug log on big-endian architectures. Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ba24040d05eb..95c7b09c6743 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7459,7 +7459,8 @@ int smb2_cancel(struct ksmbd_work *work) hdr = ksmbd_resp_buf_next(work); ksmbd_debug(SMB, "smb2 cancel called on mid %llu, async flags 0x%x\n", - hdr->MessageId, hdr->Flags); + le64_to_cpu(hdr->MessageId), + le32_to_cpu(hdr->Flags)); if (hdr->Flags & SMB2_FLAGS_ASYNC_COMMAND) { command_list = &conn->async_requests; From b69be2c58615950ee7353b61a21acdf8508c0cbb Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:32:44 +0900 Subject: [PATCH 03/50] ksmbd: validate SMB2 lease create contexts Validate SMB2 lease context lengths, requested lease state bits, and v2 flags before using the context. Return errors via ERR_PTR so CREATE can distinguish a missing lease context from a malformed one. Also ignore lease v2 contexts for SMB 2.1, where they are not valid. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 33 ++++++++++++++++++++++++++++----- fs/smb/server/smb2pdu.c | 24 +++++++++++++++++++++++- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 60e7e821c245..5c6c0550a477 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -5,6 +5,7 @@ */ #include +#include #include "glob.h" #include "oplock.h" @@ -19,6 +20,20 @@ static LIST_HEAD(lease_table_list); static DEFINE_RWLOCK(lease_list_lock); +#define SMB2_LEASE_STATE_MASK_LE (SMB2_LEASE_READ_CACHING_LE | \ + SMB2_LEASE_HANDLE_CACHING_LE | \ + SMB2_LEASE_WRITE_CACHING_LE) + +static bool lease_state_valid(__le32 state) +{ + return !(state & ~SMB2_LEASE_STATE_MASK_LE); +} + +static bool lease_v2_flags_valid(__le32 flags) +{ + return !(flags & ~SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE); +} + /** * alloc_opinfo() - allocate a new opinfo object for oplock info * @work: smb work @@ -1537,12 +1552,14 @@ struct lease_ctx_info *parse_lease_state(void *open_req) struct lease_ctx_info *lreq; cc = smb2_find_context_vals(req, SMB2_CREATE_REQUEST_LEASE, 4); - if (IS_ERR_OR_NULL(cc)) + if (IS_ERR(cc)) + return ERR_CAST(cc); + if (!cc) return NULL; lreq = kzalloc_obj(struct lease_ctx_info, KSMBD_DEFAULT_GFP); if (!lreq) - return NULL; + return ERR_PTR(-ENOMEM); if (sizeof(struct lease_context_v2) == le32_to_cpu(cc->DataLength)) { struct create_lease_v2 *lc = (struct create_lease_v2 *)cc; @@ -1556,11 +1573,14 @@ struct lease_ctx_info *parse_lease_state(void *open_req) lreq->flags = lc->lcontext.LeaseFlags; lreq->epoch = lc->lcontext.Epoch; lreq->duration = lc->lcontext.LeaseDuration; + if (!lease_state_valid(lreq->req_state) || + !lease_v2_flags_valid(lreq->flags)) + goto err_out; if (lreq->flags == SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) memcpy(lreq->parent_lease_key, lc->lcontext.ParentLeaseKey, SMB2_LEASE_KEY_SIZE); lreq->version = 2; - } else { + } else if (sizeof(struct lease_context) == le32_to_cpu(cc->DataLength)) { struct create_lease *lc = (struct create_lease *)cc; if (le16_to_cpu(cc->DataOffset) + le32_to_cpu(cc->DataLength) < @@ -1571,12 +1591,15 @@ struct lease_ctx_info *parse_lease_state(void *open_req) lreq->req_state = lc->lcontext.LeaseState; lreq->flags = lc->lcontext.LeaseFlags; lreq->duration = lc->lcontext.LeaseDuration; + if (!lease_state_valid(lreq->req_state)) + goto err_out; lreq->version = 1; - } + } else + goto err_out; return lreq; err_out: kfree(lreq); - return NULL; + return ERR_PTR(-EINVAL); } /** diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 95c7b09c6743..19e819898fd0 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3108,6 +3108,17 @@ int smb2_open(struct ksmbd_work *work) if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE && req->CreateContextsOffset) { lc = parse_lease_state(req); + if (IS_ERR(lc)) { + rc = PTR_ERR(lc); + lc = NULL; + goto err_out2; + } + if (lc && lc->version == 2 && conn->dialect < SMB30_PROT_ID) { + kfree(lc); + lc = NULL; + if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) + req_op_level = SMB2_OPLOCK_LEVEL_NONE; + } rc = parse_durable_handle_context(work, req, lc, &dh_info); if (rc) { ksmbd_debug(SMB, "error parsing durable handle context\n"); @@ -3139,8 +3150,19 @@ int smb2_open(struct ksmbd_work *work) goto reconnected_fp; } - } else if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) + } else if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) { lc = parse_lease_state(req); + if (IS_ERR(lc)) { + rc = PTR_ERR(lc); + lc = NULL; + goto err_out2; + } + if (lc && lc->version == 2 && conn->dialect < SMB30_PROT_ID) { + kfree(lc); + lc = NULL; + req_op_level = SMB2_OPLOCK_LEVEL_NONE; + } + } if (le32_to_cpu(req->ImpersonationLevel) > le32_to_cpu(IL_DELEGATE)) { pr_err("Invalid impersonationlevel : 0x%x\n", From fa111daae1a02dbff5693dfc12f368bccd9eb5f4 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:33:20 +0900 Subject: [PATCH 04/50] ksmbd: use connection ClientGUID for lease lookup MS-SMB2 defines the lease table lookup key as Connection.ClientGuid. Use the connection ClientGUID consistently when checking for same-client leases and duplicate lease keys. Also preserve directory and parent lease metadata when copying an existing lease state to a new open. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 16 +++++++++------- fs/smb/server/oplock.h | 2 +- fs/smb/server/smb2pdu.c | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 5c6c0550a477..627cea7fd7ea 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -520,7 +520,7 @@ static inline int compare_guid_key(struct oplock_info *opinfo, * Return: oplock(lease) object on success, otherwise NULL */ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, - char *client_guid, + const char *client_guid, struct lease_ctx_info *lctx) { int ret; @@ -1012,7 +1012,7 @@ void destroy_lease_table(struct ksmbd_conn *conn) write_unlock(&lease_list_lock); } -int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, +int find_same_lease_key(struct ksmbd_conn *conn, struct ksmbd_inode *ci, struct lease_ctx_info *lctx) { struct oplock_info *opinfo; @@ -1029,7 +1029,7 @@ int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, } list_for_each_entry(lb, &lease_table_list, l_entry) { - if (!memcmp(lb->client_guid, sess->ClientGUID, + if (!memcmp(lb->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) goto found; } @@ -1045,7 +1045,7 @@ int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, rcu_read_unlock(); if (opinfo->o_fp->f_ci == ci) goto op_next; - err = compare_guid_key(opinfo, sess->ClientGUID, + err = compare_guid_key(opinfo, conn->ClientGUID, lctx->lease_key); if (err) { err = -EINVAL; @@ -1078,6 +1078,9 @@ static void copy_lease(struct oplock_info *op1, struct oplock_info *op2) lease2->flags = lease1->flags; lease2->epoch = lease1->epoch; lease2->version = lease1->version; + lease2->is_dir = lease1->is_dir; + memcpy(lease2->parent_lease_key, lease1->parent_lease_key, + SMB2_LEASE_KEY_SIZE); } static void add_lease_global_list(struct oplock_info *opinfo, @@ -1216,7 +1219,6 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct ksmbd_file *fp, __u16 tid, struct lease_ctx_info *lctx, int share_ret) { - struct ksmbd_session *sess = work->sess; int err = 0; struct oplock_info *opinfo = NULL, *prev_opinfo = NULL; struct ksmbd_inode *ci = fp->f_ci; @@ -1259,12 +1261,12 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct oplock_info *m_opinfo; /* is lease already granted ? */ - m_opinfo = same_client_has_lease(ci, sess->ClientGUID, + m_opinfo = same_client_has_lease(ci, work->conn->ClientGUID, lctx); if (m_opinfo) { copy_lease(m_opinfo, opinfo); if (atomic_read(&m_opinfo->breaking_cnt)) - opinfo->o_lease->flags = + opinfo->o_lease->flags |= SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; opinfo_put(m_opinfo); goto out; diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index d91a8266e065..795a9119dad9 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -116,7 +116,7 @@ void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp); struct create_context *smb2_find_context_vals(void *open_req, const char *tag, int tag_len); struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, char *lease_key); -int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, +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 smb_send_parent_lease_break_noti(struct ksmbd_file *fp, diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 19e819898fd0..0766f0d662be 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3637,7 +3637,7 @@ int smb2_open(struct ksmbd_work *work) ksmbd_debug(SMB, "lease req for(%s) req oplock state 0x%x, lease state 0x%x\n", name, req_op_level, lc->req_state); - rc = find_same_lease_key(sess, fp->f_ci, lc); + rc = find_same_lease_key(conn, fp->f_ci, lc); if (rc) goto err_out1; } else if (open_flags == O_RDONLY && From 5015191096db311759fef98769270336cd8b1324 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:33:53 +0900 Subject: [PATCH 05/50] ksmbd: fix lease break and ack state handling Do not skip valid lease states containing WRITE_CACHING when breaking level-II/read leases for writes and truncates. Handle lease break acknowledgments according to the SMB2 rule that the acknowledged state must be a subset of the server's break target. Apply the acknowledged state directly and keep the break pending on failed ACKs. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 24 +++------ fs/smb/server/smb2pdu.c | 106 ++++++++++------------------------------ 2 files changed, 34 insertions(+), 96 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 627cea7fd7ea..cc5eb95ab3f2 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1419,14 +1419,8 @@ void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, continue; } - if (brk_op->is_lease && (brk_op->o_lease->state & - (~(SMB2_LEASE_READ_CACHING_LE | - SMB2_LEASE_HANDLE_CACHING_LE)))) { - ksmbd_debug(OPLOCK, "unexpected lease state(0x%x)\n", - brk_op->o_lease->state); - goto next; - } else if (brk_op->level != - SMB2_OPLOCK_LEVEL_II) { + if (!brk_op->is_lease && + brk_op->level != SMB2_OPLOCK_LEVEL_II) { ksmbd_debug(OPLOCK, "unexpected oplock(0x%x)\n", brk_op->level); goto next; @@ -1478,15 +1472,13 @@ void smb_break_all_oplock(struct ksmbd_work *work, struct ksmbd_file *fp) */ __u8 smb2_map_lease_to_oplock(__le32 lease_state) { - if (lease_state == (SMB2_LEASE_HANDLE_CACHING_LE | - SMB2_LEASE_READ_CACHING_LE | - SMB2_LEASE_WRITE_CACHING_LE)) { + if ((lease_state & SMB2_LEASE_WRITE_CACHING_LE) && + (lease_state & SMB2_LEASE_HANDLE_CACHING_LE)) { return SMB2_OPLOCK_LEVEL_BATCH; - } else if (lease_state != SMB2_LEASE_WRITE_CACHING_LE && - lease_state & SMB2_LEASE_WRITE_CACHING_LE) { - if (!(lease_state & SMB2_LEASE_HANDLE_CACHING_LE)) - return SMB2_OPLOCK_LEVEL_EXCLUSIVE; - } else if (lease_state & SMB2_LEASE_READ_CACHING_LE) { + } else if (lease_state & SMB2_LEASE_WRITE_CACHING_LE) { + return SMB2_OPLOCK_LEVEL_EXCLUSIVE; + } else if (lease_state & (SMB2_LEASE_READ_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE)) { return SMB2_OPLOCK_LEVEL_II; } return 0; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 0766f0d662be..75ee9184e553 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9013,16 +9013,17 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) ksmbd_fd_put(work, fp); } +static bool smb2_lease_state_valid(__le32 state) +{ + return !(state & ~(SMB2_LEASE_READ_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE | + SMB2_LEASE_WRITE_CACHING_LE)); +} + static int check_lease_state(struct lease *lease, __le32 req_state) { - if ((lease->new_state == - (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) && - !(req_state & SMB2_LEASE_WRITE_CACHING_LE)) { - lease->new_state = req_state; - return 0; - } - - if (lease->new_state == req_state) + if (smb2_lease_state_valid(req_state) && + !(req_state & ~lease->new_state)) return 0; return 1; @@ -9040,9 +9041,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; - __le32 err = 0; int ret = 0; - unsigned int lease_change_type; __le32 lease_state; struct lease *lease; @@ -9066,6 +9065,11 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) goto err_out; } + if (!atomic_read(&opinfo->breaking_cnt)) { + rsp->hdr.Status = STATUS_UNSUCCESSFUL; + goto err_out; + } + if (check_lease_state(lease, req->LeaseState)) { rsp->hdr.Status = STATUS_REQUEST_NOT_ACCEPTED; ksmbd_debug(OPLOCK, @@ -9074,72 +9078,10 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) goto err_out; } - if (!atomic_read(&opinfo->breaking_cnt)) { - rsp->hdr.Status = STATUS_UNSUCCESSFUL; - goto err_out; - } - - /* check for bad lease state */ - if (req->LeaseState & - (~(SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE))) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - lease_change_type = OPLOCK_WRITE_TO_NONE; - else - lease_change_type = OPLOCK_READ_TO_NONE; - ksmbd_debug(OPLOCK, "handle bad lease state 0x%x -> 0x%x\n", - le32_to_cpu(lease->state), - le32_to_cpu(req->LeaseState)); - } else if (lease->state == SMB2_LEASE_READ_CACHING_LE && - req->LeaseState != SMB2_LEASE_NONE_LE) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - lease_change_type = OPLOCK_READ_TO_NONE; - ksmbd_debug(OPLOCK, "handle bad lease state 0x%x -> 0x%x\n", - le32_to_cpu(lease->state), - le32_to_cpu(req->LeaseState)); - } else { - /* valid lease state changes */ - err = STATUS_INVALID_DEVICE_STATE; - if (req->LeaseState == SMB2_LEASE_NONE_LE) { - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - lease_change_type = OPLOCK_WRITE_TO_NONE; - else - lease_change_type = OPLOCK_READ_TO_NONE; - } else if (req->LeaseState & SMB2_LEASE_READ_CACHING_LE) { - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - lease_change_type = OPLOCK_WRITE_TO_READ; - else - lease_change_type = OPLOCK_READ_HANDLE_TO_READ; - } else { - lease_change_type = 0; - } - } - - switch (lease_change_type) { - case OPLOCK_WRITE_TO_READ: - ret = opinfo_write_to_read(opinfo); - break; - case OPLOCK_READ_HANDLE_TO_READ: - ret = opinfo_read_handle_to_read(opinfo); - break; - case OPLOCK_WRITE_TO_NONE: - ret = opinfo_write_to_none(opinfo); - break; - case OPLOCK_READ_TO_NONE: - ret = opinfo_read_to_none(opinfo); - break; - default: - ksmbd_debug(OPLOCK, "unknown lease change 0x%x -> 0x%x\n", - le32_to_cpu(lease->state), - le32_to_cpu(req->LeaseState)); - } - - if (ret < 0) { - rsp->hdr.Status = err; - goto err_out; - } - - lease_state = lease->state; + lease_state = req->LeaseState; + lease->state = lease_state; + lease->new_state = SMB2_LEASE_NONE_LE; + opinfo->level = smb2_map_lease_to_oplock(lease_state); rsp->StructureSize = cpu_to_le16(36); rsp->Reserved = 0; @@ -9148,16 +9090,20 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) rsp->LeaseState = lease_state; rsp->LeaseDuration = 0; ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lease_ack)); - if (ret) { -err_out: - smb2_set_err_rsp(work); - } + if (ret) + goto err_out; opinfo->op_state = OPLOCK_STATE_NONE; wake_up_interruptible_all(&opinfo->oplock_q); atomic_dec(&opinfo->breaking_cnt); wake_up_interruptible_all(&opinfo->oplock_brk); opinfo_put(opinfo); + return; + +err_out: + smb2_set_err_rsp(work); + opinfo_put(opinfo); + return; } /** From 0fa8abc6ab6dcc571f76c1e05ad3582540fb56d5 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:34:24 +0900 Subject: [PATCH 06/50] ksmbd: clean up lease response flags and directory leases Do not echo reserved v1 lease flags back to clients. For lease v2 responses, only return BREAK_IN_PROGRESS and PARENT_LEASE_KEY_SET when they are meaningful, and preserve the parent lease key in the response. Allow directory leases whenever the request is a valid lease v2 request, and initialize v2 lease epochs from the first server-granted state change. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index cc5eb95ab3f2..8ae6a6afbe04 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -34,6 +34,11 @@ static bool lease_v2_flags_valid(__le32 flags) return !(flags & ~SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE); } +static bool lease_has_parent_key(struct lease *lease) +{ + return lease->flags & SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE; +} + /** * alloc_opinfo() - allocate a new opinfo object for oplock info * @work: smb work @@ -126,7 +131,7 @@ static int alloc_lease(struct oplock_info *opinfo, struct lease_ctx_info *lctx) lease->is_dir = lctx->is_dir; memcpy(lease->parent_lease_key, lctx->parent_lease_key, SMB2_LEASE_KEY_SIZE); lease->version = lctx->version; - lease->epoch = le16_to_cpu(lctx->epoch) + 1; + lease->epoch = lctx->version == 2 ? 1 : 0; INIT_LIST_HEAD(&opinfo->lease_entry); opinfo->o_lease = lease; @@ -1228,9 +1233,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, /* Only v2 leases handle the directory */ if (S_ISDIR(file_inode(fp->filp)->i_mode)) { - if (!lctx || lctx->version != 2 || - (lctx->flags != SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE && - !lctx->epoch)) + if (!lctx || lctx->version != 2) return 0; } @@ -1493,14 +1496,17 @@ void create_lease_buf(u8 *rbuf, struct lease *lease) { if (lease->version == 2) { struct create_lease_v2 *buf = (struct create_lease_v2 *)rbuf; + __le32 flags = lease->flags & SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; memset(buf, 0, sizeof(struct create_lease_v2)); memcpy(buf->lcontext.LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE); - buf->lcontext.LeaseFlags = lease->flags; + if (lease_has_parent_key(lease)) + flags |= SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE; + buf->lcontext.LeaseFlags = flags; buf->lcontext.Epoch = cpu_to_le16(lease->epoch); buf->lcontext.LeaseState = lease->state; - if (lease->flags == SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) + if (lease_has_parent_key(lease)) memcpy(buf->lcontext.ParentLeaseKey, lease->parent_lease_key, SMB2_LEASE_KEY_SIZE); buf->ccontext.DataOffset = cpu_to_le16(offsetof @@ -1518,7 +1524,8 @@ void create_lease_buf(u8 *rbuf, struct lease *lease) memset(buf, 0, sizeof(struct create_lease)); memcpy(buf->lcontext.LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE); - buf->lcontext.LeaseFlags = lease->flags; + buf->lcontext.LeaseFlags = + lease->flags & SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; buf->lcontext.LeaseState = lease->state; buf->ccontext.DataOffset = cpu_to_le16(offsetof (struct create_lease, lcontext)); @@ -1583,7 +1590,7 @@ struct lease_ctx_info *parse_lease_state(void *open_req) memcpy(lreq->lease_key, lc->lcontext.LeaseKey, SMB2_LEASE_KEY_SIZE); lreq->req_state = lc->lcontext.LeaseState; - lreq->flags = lc->lcontext.LeaseFlags; + lreq->flags = 0; lreq->duration = lc->lcontext.LeaseDuration; if (!lease_state_valid(lreq->req_state)) goto err_out; From 079927f5fda5a05355a620d794e2f4b9eb1f70fd Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:35:06 +0900 Subject: [PATCH 07/50] ksmbd: share SMB2 lease state across opens Model SMB2 leases as per-client/per-key objects instead of keeping a separate lease copy in every oplock_info. The lease table now stores lease objects and each lease tracks the opens that reference it. This makes same ClientGuid/LeaseKey opens observe a single lease state, so lease upgrades, breaks, ACKs, and close teardown do not diverge across per-open copies. Keep one reference for the lease table entry and one reference for each open, and remove the table entry when the last open is detached. Update lease break ACK handling to refresh all open oplock levels from the shared lease state. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 262 ++++++++++++++++++++++------------------ fs/smb/server/oplock.h | 6 + fs/smb/server/smb2pdu.c | 2 +- 3 files changed, 151 insertions(+), 119 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 8ae6a6afbe04..81716317bb61 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -65,6 +65,7 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work, opinfo->fid = id; opinfo->Tid = Tid; INIT_LIST_HEAD(&opinfo->op_entry); + INIT_LIST_HEAD(&opinfo->lease_entry); init_waitqueue_head(&opinfo->oplock_q); init_waitqueue_head(&opinfo->oplock_brk); atomic_set(&opinfo->refcount, 1); @@ -73,31 +74,44 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work, return opinfo; } -static void lease_add_list(struct oplock_info *opinfo) +static void lease_get(struct lease *lease) { - struct lease_table *lb = opinfo->o_lease->l_lb; + atomic_inc(&lease->refcount); +} +static void lease_put(struct lease *lease) +{ + if (lease && atomic_dec_and_test(&lease->refcount)) + kfree(lease); +} + +static void lease_add_table(struct lease *lease, struct lease_table *lb) +{ + lease_get(lease); + lease->l_lb = lb; spin_lock(&lb->lb_lock); - list_add_rcu(&opinfo->lease_entry, &lb->lease_list); + list_add_rcu(&lease->l_entry, &lb->lease_list); spin_unlock(&lb->lb_lock); } -static void lease_del_list(struct oplock_info *opinfo) +static void lease_del_table(struct lease *lease) { - struct lease_table *lb = opinfo->o_lease->l_lb; + struct lease_table *lb = lease->l_lb; if (!lb) return; spin_lock(&lb->lb_lock); - if (list_empty(&opinfo->lease_entry)) { + if (list_empty(&lease->l_entry)) { spin_unlock(&lb->lb_lock); return; } - list_del_init(&opinfo->lease_entry); - opinfo->o_lease->l_lb = NULL; + list_del_init(&lease->l_entry); + lease->l_lb = NULL; spin_unlock(&lb->lb_lock); + + lease_put(lease); } static struct lease_table *alloc_lease_table(struct oplock_info *opinfo) @@ -115,13 +129,14 @@ static struct lease_table *alloc_lease_table(struct oplock_info *opinfo) return lb; } -static int alloc_lease(struct oplock_info *opinfo, struct lease_ctx_info *lctx) +static struct lease *alloc_lease(struct lease_ctx_info *lctx, + struct ksmbd_inode *ci) { struct lease *lease; lease = kmalloc_obj(struct lease, KSMBD_DEFAULT_GFP); if (!lease) - return -ENOMEM; + return NULL; memcpy(lease->lease_key, lctx->lease_key, SMB2_LEASE_KEY_SIZE); lease->state = lctx->req_state; @@ -132,18 +147,48 @@ static int alloc_lease(struct oplock_info *opinfo, struct lease_ctx_info *lctx) memcpy(lease->parent_lease_key, lctx->parent_lease_key, SMB2_LEASE_KEY_SIZE); lease->version = lctx->version; lease->epoch = lctx->version == 2 ? 1 : 0; - INIT_LIST_HEAD(&opinfo->lease_entry); - opinfo->o_lease = lease; + lease->ci = ci; + lease->l_lb = NULL; + INIT_LIST_HEAD(&lease->l_entry); + INIT_LIST_HEAD(&lease->open_list); + spin_lock_init(&lease->lock); + atomic_set(&lease->refcount, 1); - return 0; + return lease; +} + +static void lease_add_open(struct lease *lease, struct oplock_info *opinfo) +{ + spin_lock(&lease->lock); + list_add(&opinfo->lease_entry, &lease->open_list); + spin_unlock(&lease->lock); +} + +static void lease_del_open(struct oplock_info *opinfo) +{ + struct lease *lease = opinfo->o_lease; + bool remove_table = false; + + if (!lease) + return; + + spin_lock(&lease->lock); + if (!list_empty(&opinfo->lease_entry)) { + list_del_init(&opinfo->lease_entry); + remove_table = list_empty(&lease->open_list); + } + spin_unlock(&lease->lock); + + if (remove_table) { + write_lock(&lease_list_lock); + lease_del_table(lease); + write_unlock(&lease_list_lock); + } } static void free_lease(struct oplock_info *opinfo) { - struct lease *lease; - - lease = opinfo->o_lease; - kfree(lease); + lease_put(opinfo->o_lease); } static void __free_opinfo(struct oplock_info *opinfo) @@ -166,6 +211,21 @@ static void free_opinfo(struct oplock_info *opinfo) call_rcu(&opinfo->rcu, free_opinfo_rcu); } +void lease_update_oplock_levels(struct lease *lease) +{ + struct oplock_info *opinfo; + __u8 level; + + if (!lease) + return; + + level = smb2_map_lease_to_oplock(lease->state); + spin_lock(&lease->lock); + list_for_each_entry(opinfo, &lease->open_list, lease_entry) + opinfo->level = level; + spin_unlock(&lease->lock); +} + struct oplock_info *opinfo_get(struct ksmbd_file *fp) { struct oplock_info *opinfo; @@ -226,11 +286,9 @@ static void opinfo_del(struct oplock_info *opinfo) { struct ksmbd_inode *ci = opinfo->o_fp->f_ci; - if (opinfo->is_lease) { - write_lock(&lease_list_lock); - lease_del_list(opinfo); - write_unlock(&lease_list_lock); - } + if (opinfo->is_lease) + lease_del_open(opinfo); + down_write(&ci->m_lock); list_del(&opinfo->op_entry); up_write(&ci->m_lock); @@ -279,8 +337,10 @@ int opinfo_write_to_read(struct oplock_info *opinfo) } opinfo->level = SMB2_OPLOCK_LEVEL_II; - if (opinfo->is_lease) + if (opinfo->is_lease) { lease->state = lease->new_state; + lease_update_oplock_levels(lease); + } return 0; } @@ -295,7 +355,7 @@ int opinfo_read_handle_to_read(struct oplock_info *opinfo) struct lease *lease = opinfo->o_lease; lease->state = lease->new_state; - opinfo->level = SMB2_OPLOCK_LEVEL_II; + lease_update_oplock_levels(lease); return 0; } @@ -317,8 +377,10 @@ int opinfo_write_to_none(struct oplock_info *opinfo) return -EINVAL; } opinfo->level = SMB2_OPLOCK_LEVEL_NONE; - if (opinfo->is_lease) + if (opinfo->is_lease) { lease->state = lease->new_state; + lease_update_oplock_levels(lease); + } return 0; } @@ -339,8 +401,10 @@ int opinfo_read_to_none(struct oplock_info *opinfo) return -EINVAL; } opinfo->level = SMB2_OPLOCK_LEVEL_NONE; - if (opinfo->is_lease) + if (opinfo->is_lease) { lease->state = lease->new_state; + lease_update_oplock_levels(lease); + } return 0; } @@ -361,10 +425,7 @@ int lease_read_to_write(struct oplock_info *opinfo) lease->new_state = SMB2_LEASE_NONE_LE; lease->state |= SMB2_LEASE_WRITE_CACHING_LE; - if (lease->state & SMB2_LEASE_HANDLE_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_BATCH; - else - opinfo->level = SMB2_OPLOCK_LEVEL_EXCLUSIVE; + lease_update_oplock_levels(lease); return 0; } @@ -386,15 +447,7 @@ static int lease_none_upgrade(struct oplock_info *opinfo, __le32 new_state) lease->new_state = SMB2_LEASE_NONE_LE; lease->state = new_state; - if (lease->state & SMB2_LEASE_HANDLE_CACHING_LE) - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_BATCH; - else - opinfo->level = SMB2_OPLOCK_LEVEL_II; - else if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_EXCLUSIVE; - else if (lease->state & SMB2_LEASE_READ_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_II; + lease_update_oplock_levels(lease); return 0; } @@ -577,6 +630,7 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, SMB2_LEASE_HANDLE_CACHING_LE)) { lease->epoch++; lease->state = lctx->req_state; + lease_update_oplock_levels(lease); } } @@ -603,8 +657,10 @@ static void wait_for_break_ack(struct oplock_info *opinfo) /* is this a timeout ? */ if (!rc) { - if (opinfo->is_lease) + 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; } @@ -884,8 +940,8 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo) } else { __smb2_lease_break_noti(&work->work); if (opinfo->o_lease->new_state == SMB2_LEASE_NONE_LE) { - opinfo->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->o_lease->state = SMB2_LEASE_NONE_LE; + lease_update_oplock_levels(opinfo->o_lease); } } return 0; @@ -990,7 +1046,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, void destroy_lease_table(struct ksmbd_conn *conn) { struct lease_table *lb, *lbtmp; - struct oplock_info *opinfo; + struct lease *lease, *ltmp; write_lock(&lease_list_lock); if (list_empty(&lease_table_list)) { @@ -1002,15 +1058,8 @@ void destroy_lease_table(struct ksmbd_conn *conn) if (conn && memcmp(lb->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) continue; -again: - rcu_read_lock(); - list_for_each_entry_rcu(opinfo, &lb->lease_list, - lease_entry) { - rcu_read_unlock(); - lease_del_list(opinfo); - goto again; - } - rcu_read_unlock(); + list_for_each_entry_safe(lease, ltmp, &lb->lease_list, l_entry) + lease_del_table(lease); list_del(&lb->l_entry); kfree(lb); } @@ -1020,7 +1069,7 @@ void destroy_lease_table(struct ksmbd_conn *conn) int find_same_lease_key(struct ksmbd_conn *conn, struct ksmbd_inode *ci, struct lease_ctx_info *lctx) { - struct oplock_info *opinfo; + struct lease *lease; int err = 0; struct lease_table *lb; @@ -1043,70 +1092,40 @@ int find_same_lease_key(struct ksmbd_conn *conn, struct ksmbd_inode *ci, return 0; found: - rcu_read_lock(); - list_for_each_entry_rcu(opinfo, &lb->lease_list, lease_entry) { - if (!atomic_inc_not_zero(&opinfo->refcount)) + list_for_each_entry(lease, &lb->lease_list, l_entry) { + if (lease->ci == ci) continue; - rcu_read_unlock(); - if (opinfo->o_fp->f_ci == ci) - goto op_next; - err = compare_guid_key(opinfo, conn->ClientGUID, - lctx->lease_key); - if (err) { + if (!memcmp(lease->lease_key, lctx->lease_key, + SMB2_LEASE_KEY_SIZE)) { err = -EINVAL; ksmbd_debug(OPLOCK, "found same lease key is already used in other files\n"); - opinfo_put(opinfo); goto out; } -op_next: - opinfo_put(opinfo); - rcu_read_lock(); } - rcu_read_unlock(); out: read_unlock(&lease_list_lock); return err; } -static void copy_lease(struct oplock_info *op1, struct oplock_info *op2) -{ - struct lease *lease1 = op1->o_lease; - struct lease *lease2 = op2->o_lease; - - op2->level = op1->level; - lease2->state = lease1->state; - memcpy(lease2->lease_key, lease1->lease_key, - SMB2_LEASE_KEY_SIZE); - lease2->duration = lease1->duration; - lease2->flags = lease1->flags; - lease2->epoch = lease1->epoch; - lease2->version = lease1->version; - lease2->is_dir = lease1->is_dir; - memcpy(lease2->parent_lease_key, lease1->parent_lease_key, - SMB2_LEASE_KEY_SIZE); -} - -static void add_lease_global_list(struct oplock_info *opinfo, +static void add_lease_global_list(struct lease *lease, struct ksmbd_conn *conn, struct lease_table *new_lb) { struct lease_table *lb; write_lock(&lease_list_lock); list_for_each_entry(lb, &lease_table_list, l_entry) { - if (!memcmp(lb->client_guid, opinfo->conn->ClientGUID, + if (!memcmp(lb->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) { - opinfo->o_lease->l_lb = lb; - lease_add_list(opinfo); + lease_add_table(lease, lb); write_unlock(&lease_list_lock); kfree(new_lb); return; } } - opinfo->o_lease->l_lb = new_lb; - lease_add_list(opinfo); + lease_add_table(lease, new_lb); list_add(&new_lb->l_entry, &lease_table_list); write_unlock(&lease_list_lock); } @@ -1229,6 +1248,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct ksmbd_inode *ci = fp->f_ci; struct lease_table *new_lb = NULL; bool prev_op_has_lease; + bool new_lease = false; __le32 prev_op_state = 0; /* Only v2 leases handle the directory */ @@ -1242,10 +1262,13 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, return -ENOMEM; if (lctx) { - err = alloc_lease(opinfo, lctx); - if (err) + opinfo->o_lease = alloc_lease(lctx, ci); + if (!opinfo->o_lease) { + err = -ENOMEM; goto err_out; + } opinfo->is_lease = 1; + new_lease = true; } /* ci does not have any oplock */ @@ -1267,7 +1290,11 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, m_opinfo = same_client_has_lease(ci, work->conn->ClientGUID, lctx); if (m_opinfo) { - copy_lease(m_opinfo, opinfo); + lease_put(opinfo->o_lease); + lease_get(m_opinfo->o_lease); + opinfo->o_lease = m_opinfo->o_lease; + opinfo->level = m_opinfo->level; + new_lease = false; if (atomic_read(&m_opinfo->breaking_cnt)) opinfo->o_lease->flags |= SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; @@ -1330,17 +1357,13 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, out: /* - * Set o_fp before any publication so that concurrent readers - * (e.g. find_same_lease_key() on the lease list) that - * dereference opinfo->o_fp don't hit a NULL pointer. - * * Keep the original publication order so concurrent opens can * still observe the in-flight grant via ci->m_op_list, but make * everything after opinfo_add() no-fail by preallocating any new * lease_table first. */ opinfo->o_fp = fp; - if (opinfo->is_lease) { + if (new_lease) { new_lb = alloc_lease_table(opinfo); if (!new_lb) { err = -ENOMEM; @@ -1351,8 +1374,10 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, opinfo_count_inc(fp); opinfo_add(opinfo, fp); + if (new_lease) + add_lease_global_list(opinfo->o_lease, opinfo->conn, new_lb); if (opinfo->is_lease) - add_lease_global_list(opinfo, new_lb); + lease_add_open(opinfo->o_lease, opinfo); rcu_assign_pointer(fp->f_opinfo, opinfo); @@ -1834,8 +1859,8 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, char *lease_key) { struct oplock_info *opinfo = NULL, *ret_op = NULL; + struct lease *lease; struct lease_table *lt; - int ret; read_lock(&lease_list_lock); list_for_each_entry(lt, &lease_table_list, l_entry) { @@ -1848,29 +1873,30 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, return NULL; found: - rcu_read_lock(); - list_for_each_entry_rcu(opinfo, <->lease_list, lease_entry) { - if (!atomic_inc_not_zero(&opinfo->refcount)) + list_for_each_entry(lease, <->lease_list, l_entry) { + if (memcmp(lease->lease_key, lease_key, SMB2_LEASE_KEY_SIZE)) continue; - rcu_read_unlock(); - if (!opinfo->op_state || opinfo->op_state == OPLOCK_CLOSING) - goto op_next; - if (!(opinfo->o_lease->state & - (SMB2_LEASE_HANDLE_CACHING_LE | - SMB2_LEASE_WRITE_CACHING_LE))) - goto op_next; - ret = compare_guid_key(opinfo, conn->ClientGUID, - lease_key); - if (ret) { - ksmbd_debug(OPLOCK, "found opinfo\n"); + if (!(lease->state & (SMB2_LEASE_HANDLE_CACHING_LE | + SMB2_LEASE_WRITE_CACHING_LE))) + break; + + spin_lock(&lease->lock); + list_for_each_entry(opinfo, &lease->open_list, lease_entry) { + if (!opinfo->op_state || + opinfo->op_state == OPLOCK_CLOSING) + continue; + if (!atomic_inc_not_zero(&opinfo->refcount)) + continue; ret_op = opinfo; + break; + } + spin_unlock(&lease->lock); + if (ret_op) { + ksmbd_debug(OPLOCK, "found opinfo\n"); goto out; } -op_next: - opinfo_put(opinfo); - rcu_read_lock(); + break; } - rcu_read_unlock(); out: read_unlock(&lease_list_lock); diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 795a9119dad9..30bad6d65048 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -49,7 +49,12 @@ struct lease { int version; unsigned short epoch; bool is_dir; + struct ksmbd_inode *ci; struct lease_table *l_lb; + struct list_head l_entry; + struct list_head open_list; + spinlock_t lock; + atomic_t refcount; }; struct oplock_info { @@ -105,6 +110,7 @@ void opinfo_put(struct oplock_info *opinfo); void create_lease_buf(u8 *rbuf, struct lease *lease); struct lease_ctx_info *parse_lease_state(void *open_req); __u8 smb2_map_lease_to_oplock(__le32 lease_state); +void lease_update_oplock_levels(struct lease *lease); int lease_read_to_write(struct oplock_info *opinfo); /* Durable related functions */ diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 75ee9184e553..f3a57a32c4a8 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9081,7 +9081,7 @@ 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; - opinfo->level = smb2_map_lease_to_oplock(lease_state); + lease_update_oplock_levels(lease); rsp->StructureSize = cpu_to_le16(36); rsp->Reserved = 0; From 80a56d4a826c6c84430286fcf7d8655f7c5b0868 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:35:42 +0900 Subject: [PATCH 08/50] ksmbd: align SMB2 oplock break ack handling Handle SMB2 oplock break acknowledgments according to the server-side validation rules in MS-SMB2. Return STATUS_INVALID_DEVICE_STATE when an ACK arrives while the open is not breaking, reject SMB2_OPLOCK_LEVEL_LEASE with STATUS_INVALID_PARAMETER, allow BATCH acknowledgments to EXCLUSIVE, and make invalid ACK levels fail with STATUS_INVALID_OPLOCK_PROTOCOL after lowering the oplock to NONE. Update the successful response from the final granted oplock level instead of relying on the oplock transition helpers, which could turn invalid ACKs into successful responses. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 108 ++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f3a57a32c4a8..b84062b16a75 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8900,11 +8900,10 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) struct smb2_oplock_break *rsp; struct ksmbd_file *fp; struct oplock_info *opinfo = NULL; - __le32 err = 0; - int ret = 0; + __le32 status = STATUS_SUCCESS; + int ret; u64 volatile_id, persistent_id; char req_oplevel = 0, rsp_oplevel = 0; - unsigned int oplock_change_type; WORK_BUFFERS(work, req, rsp); @@ -8930,70 +8929,54 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) return; } + if (opinfo->op_state != OPLOCK_ACK_WAIT) { + ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", + opinfo->op_state); + status = STATUS_INVALID_DEVICE_STATE; + goto err_out; + } + + if (req_oplevel == SMB2_OPLOCK_LEVEL_LEASE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_PARAMETER; + goto err_out; + } + if (opinfo->level == SMB2_OPLOCK_LEVEL_NONE) { - rsp->hdr.Status = STATUS_INVALID_OPLOCK_PROTOCOL; + status = STATUS_INVALID_OPLOCK_PROTOCOL; goto err_out; } - if (opinfo->op_state == OPLOCK_STATE_NONE) { - ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", opinfo->op_state); - rsp->hdr.Status = STATUS_UNSUCCESSFUL; + if (opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE && + req_oplevel != SMB2_OPLOCK_LEVEL_II && + req_oplevel != SMB2_OPLOCK_LEVEL_NONE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_OPLOCK_PROTOCOL; goto err_out; } - if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || - opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) && - (req_oplevel != SMB2_OPLOCK_LEVEL_II && - req_oplevel != SMB2_OPLOCK_LEVEL_NONE)) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - oplock_change_type = OPLOCK_WRITE_TO_NONE; - } else if (opinfo->level == SMB2_OPLOCK_LEVEL_II && - req_oplevel != SMB2_OPLOCK_LEVEL_NONE) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - oplock_change_type = OPLOCK_READ_TO_NONE; - } else if (req_oplevel == SMB2_OPLOCK_LEVEL_II || - req_oplevel == SMB2_OPLOCK_LEVEL_NONE) { - err = STATUS_INVALID_DEVICE_STATE; - if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || - opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) && - req_oplevel == SMB2_OPLOCK_LEVEL_II) { - oplock_change_type = OPLOCK_WRITE_TO_READ; - } else if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || - opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) && - req_oplevel == SMB2_OPLOCK_LEVEL_NONE) { - oplock_change_type = OPLOCK_WRITE_TO_NONE; - } else if (opinfo->level == SMB2_OPLOCK_LEVEL_II && - req_oplevel == SMB2_OPLOCK_LEVEL_NONE) { - oplock_change_type = OPLOCK_READ_TO_NONE; - } else { - oplock_change_type = 0; - } - } else { - oplock_change_type = 0; + if (opinfo->level == SMB2_OPLOCK_LEVEL_BATCH && + req_oplevel != SMB2_OPLOCK_LEVEL_II && + req_oplevel != SMB2_OPLOCK_LEVEL_NONE && + req_oplevel != SMB2_OPLOCK_LEVEL_EXCLUSIVE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_OPLOCK_PROTOCOL; + goto err_out; } - switch (oplock_change_type) { - case OPLOCK_WRITE_TO_READ: - ret = opinfo_write_to_read(opinfo); - rsp_oplevel = SMB2_OPLOCK_LEVEL_II; - break; - case OPLOCK_WRITE_TO_NONE: - ret = opinfo_write_to_none(opinfo); + if (opinfo->level == SMB2_OPLOCK_LEVEL_II && + req_oplevel != SMB2_OPLOCK_LEVEL_NONE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_OPLOCK_PROTOCOL; + goto err_out; + } + + if (req_oplevel == SMB2_OPLOCK_LEVEL_EXCLUSIVE) rsp_oplevel = SMB2_OPLOCK_LEVEL_NONE; - break; - case OPLOCK_READ_TO_NONE: - ret = opinfo_read_to_none(opinfo); - rsp_oplevel = SMB2_OPLOCK_LEVEL_NONE; - break; - default: - pr_err("unknown oplock change 0x%x -> 0x%x\n", - opinfo->level, rsp_oplevel); - } + else + rsp_oplevel = req_oplevel; - if (ret < 0) { - rsp->hdr.Status = err; - goto err_out; - } + opinfo->level = rsp_oplevel; rsp->StructureSize = cpu_to_le16(24); rsp->OplockLevel = rsp_oplevel; @@ -9002,11 +8985,16 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) rsp->VolatileFid = volatile_id; rsp->PersistentFid = persistent_id; ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_oplock_break)); - if (ret) { -err_out: - smb2_set_err_rsp(work); - } + if (ret) + ksmbd_debug(SMB, "failed to pin oplock break response: %d\n", + ret); + goto out; +err_out: + rsp->hdr.Status = status; + smb2_set_err_rsp(work); + +out: opinfo->op_state = OPLOCK_STATE_NONE; wake_up_interruptible_all(&opinfo->oplock_q); opinfo_put(opinfo); From 171b5d72dd80f99271c073c6e38d5263687c3b6d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:36:17 +0900 Subject: [PATCH 09/50] ksmbd: treat unnamed DATA stream as base file The SMB path suffix :: names the unnamed data stream of the base file, not an alternate data stream backed by a DosStream xattr. Canonicalize an empty stream name with an explicit type to a NULL stream name after parsing. This keeps the base filename produced by strsep() and lets open continue through the normal base-file path instead of looking for a non-existent empty stream xattr. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/misc.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/misc.c b/fs/smb/server/misc.c index 966004c414a8..2dd91c9e956a 100644 --- a/fs/smb/server/misc.c +++ b/fs/smb/server/misc.c @@ -121,7 +121,9 @@ int parse_stream_name(char *filename, char **stream_name, int *s_type) char *stream_type; char *s_name; int rc = 0; + bool has_stream_type = false; + *stream_name = NULL; s_name = filename; filename = strsep(&s_name, ":"); ksmbd_debug(SMB, "filename : %s, streams : %s\n", filename, s_name); @@ -137,14 +139,20 @@ int parse_stream_name(char *filename, char **stream_name, int *s_type) ksmbd_debug(SMB, "stream name : %s, stream type : %s\n", s_name, stream_type); - if (!strncasecmp("$data", stream_type, 5)) + if (!strncasecmp("$data", stream_type, 5)) { *s_type = DATA_STREAM; - else if (!strncasecmp("$index_allocation", stream_type, 17)) + has_stream_type = true; + } else if (!strncasecmp("$index_allocation", stream_type, 17)) { *s_type = DIR_STREAM; - else + has_stream_type = true; + } else { rc = -ENOENT; + } } + if (has_stream_type && !s_name[0] && *s_type == DATA_STREAM) + goto out; + *stream_name = s_name; out: return rc; From 890825ed5c427fcb542ae1a0fd7495e551dcacaf Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 10:36:59 +0900 Subject: [PATCH 10/50] ksmbd: compute lease break-in-progress flag on response SMB2_LEASE_FLAG_BREAK_IN_PROGRESS is a transient create response flag, not persistent lease state. Do not store the flag in lease->flags when a same-key open is granted during a pending break. Instead, derive it from lease opens that are still waiting for a break ACK while building the lease create response, and keep lease->flags for persistent lease flags such as the parent lease key. This clears the flag naturally after the break ACK completes and fixes reopen responses that report BREAK_IN_PROGRESS after the lease is no longer breaking. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 81716317bb61..b1740fe988e6 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -39,6 +39,23 @@ static bool lease_has_parent_key(struct lease *lease) return lease->flags & SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE; } +static bool lease_break_in_progress(struct lease *lease) +{ + struct oplock_info *opinfo; + bool ret = false; + + spin_lock(&lease->lock); + list_for_each_entry(opinfo, &lease->open_list, lease_entry) { + if (opinfo->op_state == OPLOCK_ACK_WAIT) { + ret = true; + break; + } + } + spin_unlock(&lease->lock); + + return ret; +} + /** * alloc_opinfo() - allocate a new opinfo object for oplock info * @work: smb work @@ -1292,15 +1309,12 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, if (m_opinfo) { lease_put(opinfo->o_lease); lease_get(m_opinfo->o_lease); - opinfo->o_lease = m_opinfo->o_lease; - opinfo->level = m_opinfo->level; - new_lease = false; - if (atomic_read(&m_opinfo->breaking_cnt)) - opinfo->o_lease->flags |= - SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; - opinfo_put(m_opinfo); - goto out; - } + opinfo->o_lease = m_opinfo->o_lease; + opinfo->level = m_opinfo->level; + new_lease = false; + opinfo_put(m_opinfo); + goto out; + } } prev_opinfo = opinfo_get_list(ci); if (!prev_opinfo || @@ -1521,13 +1535,15 @@ void create_lease_buf(u8 *rbuf, struct lease *lease) { if (lease->version == 2) { struct create_lease_v2 *buf = (struct create_lease_v2 *)rbuf; - __le32 flags = lease->flags & SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; + __le32 flags = 0; memset(buf, 0, sizeof(struct create_lease_v2)); memcpy(buf->lcontext.LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE); if (lease_has_parent_key(lease)) flags |= SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE; + if (lease_break_in_progress(lease)) + flags |= SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; buf->lcontext.LeaseFlags = flags; buf->lcontext.Epoch = cpu_to_le16(lease->epoch); buf->lcontext.LeaseState = lease->state; @@ -1549,8 +1565,9 @@ void create_lease_buf(u8 *rbuf, struct lease *lease) memset(buf, 0, sizeof(struct create_lease)); memcpy(buf->lcontext.LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE); - buf->lcontext.LeaseFlags = - lease->flags & SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; + if (lease_break_in_progress(lease)) + buf->lcontext.LeaseFlags = + SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; buf->lcontext.LeaseState = lease->state; buf->ccontext.DataOffset = cpu_to_le16(offsetof (struct create_lease, lcontext)); From 7efb3f2458a8d260ce6ab37807f4b2a926483f76 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 23 Jun 2026 08:28:55 +0900 Subject: [PATCH 11/50] ksmbd: chain pending lease breaks before waking waiters A pending open can require more than one lease break before the existing lease becomes compatible with the operation that triggered the break. smb2.lease.breaking3 expects the server to hold the pending normal open through RWH->RH and RH->R, while a later overwrite waiter must not collapse that second break directly to RH->NONE. Keep pending_break held for lease breaks until the current triggering operation is compatible with the lease state. Snapshot the truncate request per oplock_break() call so another waiter cannot overwrite the state of the active break. Use the requested oplock level when deciding whether to chain another break. A second lease open only needs RWH->RH, while a normal none-oplock open can continue down to R and then NONE. For non-truncating metadata operations, break leases only down to read caching. Operations such as delete-on-close need to drop handle caching, but should not send a second R->NONE break after the client acknowledges RH->R. Also send STATUS_PENDING for levelII/read-lease break waiters. An async SMB2 create becomes cancelable only after the server sends an NT_STATUS_PENDING interim response. A waiter that blocks behind an already active lease break must receive the interim response before sleeping on pending_break, otherwise the client can process a later lease break while the create request is still not marked pending. Avoid duplicate interim responses when an overwrite first breaks a write oplock and then scans levelII/read leases. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 108 ++++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index b1740fe988e6..608961c838c9 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -727,6 +727,17 @@ static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level) return 0; } +static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level, + bool open_trunc) +{ + struct lease *lease = opinfo->o_lease; + + if (open_trunc) + return lease->state != SMB2_LEASE_NONE_LE; + + return opinfo->level > req_op_level; +} + /** * __smb2_oplock_break_noti() - send smb2 oplock break cmd from conn * to client @@ -985,6 +996,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, struct ksmbd_work *in_work) { int err = 0; + bool sent_interim = false; /* Need to break exclusive/batch oplock, write lease or overwrite_if */ ksmbd_debug(OPLOCK, @@ -993,13 +1005,22 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (brk_opinfo->is_lease) { struct lease *lease = brk_opinfo->o_lease; + bool open_trunc = brk_opinfo->open_trunc; + + if (in_work && test_bit(0, &brk_opinfo->pending_break)) { + setup_async_work(in_work, NULL, NULL); + smb2_send_interim_resp(in_work, STATUS_PENDING); + release_async_work(in_work); + sent_interim = true; + } - atomic_inc(&brk_opinfo->breaking_cnt); err = oplock_break_pending(brk_opinfo, req_op_level); if (err) return err < 0 ? err : 0; - if (brk_opinfo->open_trunc) { +again: + atomic_inc(&brk_opinfo->breaking_cnt); + if (open_trunc) { /* * Create overwrite break trigger the lease break to * none. @@ -1024,17 +1045,31 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, } } + if (in_work && !sent_interim) { + setup_async_work(in_work, NULL, NULL); + smb2_send_interim_resp(in_work, STATUS_PENDING); + release_async_work(in_work); + sent_interim = true; + } + if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) { - if (in_work) { - setup_async_work(in_work, NULL, NULL); - smb2_send_interim_resp(in_work, STATUS_PENDING); - release_async_work(in_work); - } - brk_opinfo->op_state = OPLOCK_ACK_WAIT; } else atomic_dec(&brk_opinfo->breaking_cnt); + + err = smb2_lease_break_noti(brk_opinfo); + + ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); + if (brk_opinfo->op_state == OPLOCK_CLOSING) + err = -ENOENT; + + wait_lease_breaking(brk_opinfo); + if (!err && lease_break_needed(brk_opinfo, req_op_level, open_trunc)) + goto again; + + wake_up_oplock_break(brk_opinfo); + return err; } else { err = oplock_break_pending(brk_opinfo, req_op_level); if (err) @@ -1045,18 +1080,13 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, brk_opinfo->op_state = OPLOCK_ACK_WAIT; } - if (brk_opinfo->is_lease) - err = smb2_lease_break_noti(brk_opinfo); - else - err = smb2_oplock_break_noti(brk_opinfo); + err = smb2_oplock_break_noti(brk_opinfo); ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) err = -ENOENT; wake_up_oplock_break(brk_opinfo); - wait_lease_breaking(brk_opinfo); - return err; } @@ -1261,6 +1291,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct lease_ctx_info *lctx, int share_ret) { int err = 0; + int break_level = SMB2_OPLOCK_LEVEL_II; struct oplock_info *opinfo = NULL, *prev_opinfo = NULL; struct ksmbd_inode *ci = fp->f_ci; struct lease_table *new_lb = NULL; @@ -1325,6 +1356,9 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, prev_op_has_lease = prev_opinfo->is_lease; if (prev_op_has_lease) prev_op_state = prev_opinfo->o_lease->state; + if (prev_op_has_lease && !lctx && + prev_op_state & SMB2_LEASE_HANDLE_CACHING_LE) + break_level = SMB2_OPLOCK_LEVEL_NONE; if (share_ret < 0 && prev_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { @@ -1339,7 +1373,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, goto op_break_not_needed; } - err = oplock_break(prev_opinfo, SMB2_OPLOCK_LEVEL_II, work); + err = oplock_break(prev_opinfo, break_level, work); opinfo_put(prev_opinfo); if (err == -ENOENT) goto set_lev; @@ -1408,38 +1442,46 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, * @fp: ksmbd file pointer * @is_trunc: truncate on open */ -static void smb_break_all_write_oplock(struct ksmbd_work *work, +static bool smb_break_all_write_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc) { struct oplock_info *brk_opinfo; + bool sent_break = false; brk_opinfo = opinfo_get_list(fp->f_ci); if (!brk_opinfo) - return; + return false; if (brk_opinfo->level != SMB2_OPLOCK_LEVEL_BATCH && brk_opinfo->level != SMB2_OPLOCK_LEVEL_EXCLUSIVE) { opinfo_put(brk_opinfo); - return; + return false; } brk_opinfo->open_trunc = is_trunc; oplock_break(brk_opinfo, SMB2_OPLOCK_LEVEL_II, work); + sent_break = true; opinfo_put(brk_opinfo); + + return sent_break; } /** - * smb_break_all_levII_oplock() - send level2 oplock or read lease break command + * __smb_break_all_levII_oplock() - send level2 oplock or read lease break command * from server to client - * @work: smb work - * @fp: ksmbd file pointer - * @is_trunc: truncate on open + * @work: smb work + * @fp: ksmbd file pointer + * @is_trunc: truncate on open + * @send_interim: send interim response to the client + * @send_oplock_break: send oplock break notification to the client */ -void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, - int is_trunc) +static void __smb_break_all_levII_oplock(struct ksmbd_work *work, + struct ksmbd_file *fp, int is_trunc, + bool send_interim) { struct oplock_info *op, *brk_op; struct ksmbd_inode *ci; struct ksmbd_conn *conn = work->conn; + bool sent_interim = false; if (!test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_OPLOCKS)) @@ -1481,7 +1523,11 @@ void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, SMB2_LEASE_KEY_SIZE)) goto next; brk_op->open_trunc = is_trunc; - oplock_break(brk_op, SMB2_OPLOCK_LEVEL_NONE, NULL); + oplock_break(brk_op, + brk_op->is_lease && !is_trunc ? + SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE, + send_interim && !sent_interim ? work : NULL); + sent_interim = true; next: opinfo_put(brk_op); } @@ -1491,6 +1537,12 @@ void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, opinfo_put(op); } +void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, + int is_trunc) +{ + __smb_break_all_levII_oplock(work, fp, is_trunc, true); +} + /** * smb_break_all_oplock() - break both batch/exclusive and level2 oplock * @work: smb work @@ -1498,12 +1550,14 @@ void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, */ void smb_break_all_oplock(struct ksmbd_work *work, struct ksmbd_file *fp) { + bool sent_break; + if (!test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_OPLOCKS)) return; - smb_break_all_write_oplock(work, fp, 1); - smb_break_all_levII_oplock(work, fp, 1); + sent_break = smb_break_all_write_oplock(work, fp, 1); + __smb_break_all_levII_oplock(work, fp, 1, !sent_break); } /** From 752bbd323e779691998920035b6e9417d5a6a78c Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 21:31:53 +0900 Subject: [PATCH 12/50] ksmbd: do not wait for RH lease break ack on overwrite smb2.lease.breaking4 expects an overwrite against an RH lease to send RH->NONE lease break notification but complete the triggering create without waiting for the break ack. Keep the lease in break-in-progress state until the client eventually acknowledges the downgrade, but do not hold the overwrite request behind that ack. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 608961c838c9..4d89fbb6c9c5 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -927,7 +927,7 @@ static void __smb2_lease_break_noti(struct work_struct *wk) * * Return: 0 on success, otherwise error */ -static int smb2_lease_break_noti(struct oplock_info *opinfo) +static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack) { struct ksmbd_conn *conn; struct ksmbd_work *work; @@ -964,7 +964,8 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo) if (opinfo->op_state == OPLOCK_ACK_WAIT) { INIT_WORK(&work->work, __smb2_lease_break_noti); ksmbd_queue_work(work); - wait_for_break_ack(opinfo); + if (wait_ack) + wait_for_break_ack(opinfo); } else { __smb2_lease_break_noti(&work->work); if (opinfo->o_lease->new_state == SMB2_LEASE_NONE_LE) { @@ -1006,6 +1007,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (brk_opinfo->is_lease) { struct lease *lease = brk_opinfo->o_lease; bool open_trunc = brk_opinfo->open_trunc; + bool wait_ack; if (in_work && test_bit(0, &brk_opinfo->pending_break)) { setup_async_work(in_work, NULL, NULL); @@ -1058,14 +1060,19 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, } else atomic_dec(&brk_opinfo->breaking_cnt); - err = smb2_lease_break_noti(brk_opinfo); + wait_ack = !(open_trunc && + lease->state == (SMB2_LEASE_READ_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE)); + err = smb2_lease_break_noti(brk_opinfo, wait_ack); ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) err = -ENOENT; - wait_lease_breaking(brk_opinfo); - if (!err && lease_break_needed(brk_opinfo, req_op_level, open_trunc)) + if (wait_ack) + wait_lease_breaking(brk_opinfo); + if (wait_ack && !err && + lease_break_needed(brk_opinfo, req_op_level, open_trunc)) goto again; wake_up_oplock_break(brk_opinfo); From dc2264a9efe7b56040b018024d1dfe0b3839d5ae Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 21:32:29 +0900 Subject: [PATCH 13/50] ksmbd: honor SMB2 v2 lease epochs v2 lease responses should continue from the client supplied epoch. Initialize a new v2 lease from the requested epoch plus one so create responses match the epoch returned by Windows and expected by smbtorture. For a single chained break sequence, increment the epoch only for the first break notification. Follow-up breaks such as RH->R and R->NONE in smb2.lease.v2_breaking3 reuse the same epoch. Record when a waiter slept behind pending_break and let the later truncate/open overwrite break consume that marker to reuse the current epoch instead of assigning a new one. Do not increment the epoch when a same-client, same-key create asks for the already granted RH state. The epoch changes only when the granted lease state changes. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 39 +++++++++++++++++++++++++++++---------- fs/smb/server/oplock.h | 1 + 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 4d89fbb6c9c5..fe0bbb0e1c89 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -163,8 +163,9 @@ static struct lease *alloc_lease(struct lease_ctx_info *lctx, lease->is_dir = lctx->is_dir; memcpy(lease->parent_lease_key, lctx->parent_lease_key, SMB2_LEASE_KEY_SIZE); lease->version = lctx->version; - lease->epoch = lctx->version == 2 ? 1 : 0; + lease->epoch = lctx->version == 2 ? le16_to_cpu(lctx->epoch) + 1 : 0; lease->ci = ci; + lease->reuse_epoch = false; lease->l_lb = NULL; INIT_LIST_HEAD(&lease->l_entry); INIT_LIST_HEAD(&lease->open_list); @@ -645,9 +646,11 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, if (lctx->req_state == (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) { - lease->epoch++; - lease->state = lctx->req_state; - lease_update_oplock_levels(lease); + if (lease->state != lctx->req_state) { + lease->epoch++; + lease->state = lctx->req_state; + lease_update_oplock_levels(lease); + } } } @@ -694,6 +697,9 @@ static void wake_up_oplock_break(struct oplock_info *opinfo) static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level) { while (test_and_set_bit(0, &opinfo->pending_break)) { + if (opinfo->is_lease) + opinfo->o_lease->reuse_epoch = true; + wait_on_bit(&opinfo->pending_break, 0, TASK_UNINTERRUPTIBLE); /* Not immediately break to none. */ @@ -927,7 +933,8 @@ static void __smb2_lease_break_noti(struct work_struct *wk) * * Return: 0 on success, otherwise error */ -static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack) +static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, + bool inc_epoch) { struct ksmbd_conn *conn; struct ksmbd_work *work; @@ -950,10 +957,13 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack) br_info->curr_state = lease->state; br_info->new_state = lease->new_state; - if (lease->version == 2) - br_info->epoch = cpu_to_le16(++lease->epoch); - else + 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; @@ -1007,9 +1017,11 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (brk_opinfo->is_lease) { struct lease *lease = brk_opinfo->o_lease; bool open_trunc = brk_opinfo->open_trunc; + bool was_pending = test_bit(0, &brk_opinfo->pending_break); bool wait_ack; + bool inc_epoch = true; - if (in_work && test_bit(0, &brk_opinfo->pending_break)) { + if (in_work && was_pending) { setup_async_work(in_work, NULL, NULL); smb2_send_interim_resp(in_work, STATUS_PENDING); release_async_work(in_work); @@ -1019,6 +1031,8 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, err = oplock_break_pending(brk_opinfo, req_op_level); if (err) return err < 0 ? err : 0; + if (was_pending) + open_trunc = brk_opinfo->open_trunc; again: atomic_inc(&brk_opinfo->breaking_cnt); @@ -1063,7 +1077,12 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, wait_ack = !(open_trunc && lease->state == (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)); - err = smb2_lease_break_noti(brk_opinfo, wait_ack); + if (lease->reuse_epoch) { + inc_epoch = false; + lease->reuse_epoch = false; + } + err = smb2_lease_break_noti(brk_opinfo, wait_ack, inc_epoch); + inc_epoch = false; ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 30bad6d65048..1dd52b44557f 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -49,6 +49,7 @@ struct lease { int version; unsigned short epoch; bool is_dir; + bool reuse_epoch; struct ksmbd_inode *ci; struct lease_table *l_lb; struct list_head l_entry; From 1f1083c36fa11c5d9011451c7b9ab380545c72ea Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 21:33:07 +0900 Subject: [PATCH 14/50] ksmbd: break RH leases before delete-on-close The delete paths only marked the opened file delete pending or delete-on-close. When another client still held a read/handle lease, no lease break was sent before the delete state changed. smb2.lease.unlink uses a create request with FILE_DELETE_ON_CLOSE and expects the second client's unlink to break the first client's RH lease to R with ACK_REQUIRED set. SetInfo(FileDispositionInformation) has the same lease-breaking requirement. Break level-II/read-handle leases before setting delete pending or delete-on-close so clients are notified before the file is removed. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index b84062b16a75..f1a0bce0c1f7 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3653,8 +3653,10 @@ int smb2_open(struct ksmbd_work *work) goto err_out1; } - if (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) + if (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) { + smb_break_all_levII_oplock(work, fp, 0); ksmbd_fd_set_delete_on_close(fp, file_info); + } if (need_truncate) { rc = smb2_create_truncate(&fp->filp->f_path); @@ -6584,7 +6586,8 @@ static int set_rename_info(struct ksmbd_work *work, struct ksmbd_file *fp, return smb2_rename(work, fp, rename_info, work->conn->local_nls); } -static int set_file_disposition_info(struct ksmbd_file *fp, +static int set_file_disposition_info(struct ksmbd_work *work, + struct ksmbd_file *fp, struct smb2_file_disposition_info *file_info) { struct inode *inode; @@ -6599,6 +6602,7 @@ static int set_file_disposition_info(struct ksmbd_file *fp, if (S_ISDIR(inode->i_mode) && ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY) return -EBUSY; + smb_break_all_levII_oplock(work, fp, 0); ksmbd_set_inode_pending_delete(fp); } else { ksmbd_clear_inode_pending_delete(fp); @@ -6725,7 +6729,7 @@ static int smb2_set_info_file(struct ksmbd_work *work, struct ksmbd_file *fp, if (buf_len < sizeof(struct smb2_file_disposition_info)) return -EMSGSIZE; - return set_file_disposition_info(fp, + return set_file_disposition_info(work, fp, (struct smb2_file_disposition_info *)buffer); } case FILE_FULL_EA_INFORMATION: From 2145945feb2c27d8e20f113ef81dc373631c4f05 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 21:33:40 +0900 Subject: [PATCH 15/50] ksmbd: route v2 lease breaks on the client lease channel v2 leases are scoped by ClientGuid. When the same client uses multiple connections, smbtorture expects lease break notifications to be sent on the connection associated with the client lease table, not necessarily on the connection that owns the individual open being broken. Keep a referenced connection in the lease table and use it for v2 lease break notifications while it is still active. Fall back to the open's connection if the table connection is being released. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 17 +++++++++++++++-- fs/smb/server/oplock.h | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index fe0bbb0e1c89..d936c338f7c7 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -141,11 +141,21 @@ 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) { @@ -942,6 +952,9 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, struct lease *lease = opinfo->o_lease; conn = READ_ONCE(opinfo->conn); + if (lease->version == 2 && lease->l_lb && lease->l_lb->conn && + !ksmbd_conn_releasing(lease->l_lb->conn)) + conn = lease->l_lb->conn; if (!conn) return 0; @@ -1134,7 +1147,7 @@ void destroy_lease_table(struct ksmbd_conn *conn) list_for_each_entry_safe(lease, ltmp, &lb->lease_list, l_entry) lease_del_table(lease); list_del(&lb->l_entry); - kfree(lb); + free_lease_table(lb); } write_unlock(&lease_list_lock); } @@ -1193,7 +1206,7 @@ static void add_lease_global_list(struct lease *lease, struct ksmbd_conn *conn, SMB2_CLIENT_GUID_SIZE)) { lease_add_table(lease, lb); write_unlock(&lease_list_lock); - kfree(new_lb); + free_lease_table(new_lb); return; } } diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 1dd52b44557f..8d1943586246 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -34,6 +34,7 @@ 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; From 411398c9a1414094f4e52c8893ef55ae3c61bf2a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 18 Jun 2026 21:54:43 +0900 Subject: [PATCH 16/50] ksmbd: keep common response iovecs in the work item Most SMB responses need no more than four kvec entries, but every work item currently allocates a separate four-entry array and frees it after the response is sent. Embed the common array in struct ksmbd_work and allocate a larger array only when a response exceeds the inline capacity. This removes one allocation and one free from the common request path while preserving support for larger compound and read responses. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/ksmbd_work.c | 58 ++++++++++++++++++++++++-------------- fs/smb/server/ksmbd_work.h | 3 ++ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index a5ab6799a65c..e2c2f45264be 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -16,6 +16,36 @@ static struct kmem_cache *work_cache; static struct workqueue_struct *ksmbd_wq; +static int ksmbd_reserve_iov(struct ksmbd_work *work, int need_iov_cnt) +{ + struct kvec *new; + int new_alloc_cnt = work->iov_alloc_cnt; + + if (work->iov_alloc_cnt >= work->iov_cnt + need_iov_cnt) + return 0; + + do { + new_alloc_cnt += KSMBD_WORK_INLINE_IOVS; + } while (new_alloc_cnt < work->iov_cnt + need_iov_cnt); + + if (work->iov == work->iov_inline) { + new = kcalloc(new_alloc_cnt, sizeof(*new), KSMBD_DEFAULT_GFP); + if (!new) + return -ENOMEM; + + memcpy(new, work->iov_inline, sizeof(work->iov_inline)); + } else { + new = krealloc(work->iov, sizeof(*new) * new_alloc_cnt, + KSMBD_DEFAULT_GFP | __GFP_ZERO); + if (!new) + return -ENOMEM; + } + + work->iov = new; + work->iov_alloc_cnt = new_alloc_cnt; + return 0; +} + struct ksmbd_work *ksmbd_alloc_work_struct(void) { struct ksmbd_work *work = kmem_cache_zalloc(work_cache, KSMBD_DEFAULT_GFP); @@ -27,13 +57,8 @@ struct ksmbd_work *ksmbd_alloc_work_struct(void) INIT_LIST_HEAD(&work->async_request_entry); INIT_LIST_HEAD(&work->fp_entry); INIT_LIST_HEAD(&work->aux_read_list); - work->iov_alloc_cnt = 4; - work->iov = kzalloc_objs(struct kvec, work->iov_alloc_cnt, - KSMBD_DEFAULT_GFP); - if (!work->iov) { - kmem_cache_free(work_cache, work); - work = NULL; - } + work->iov_alloc_cnt = ARRAY_SIZE(work->iov_inline); + work->iov = work->iov_inline; } return work; } @@ -55,7 +80,8 @@ void ksmbd_free_work_struct(struct ksmbd_work *work) kfree(work->tr_buf); kvfree(work->compress_buf); kvfree(work->request_buf); - kfree(work->iov); + if (work->iov != work->iov_inline) + kfree(work->iov); if (work->async_id) ksmbd_release_id(&work->conn->async_ida, work->async_id); @@ -117,19 +143,9 @@ static int __ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len, return -ENOMEM; } - if (work->iov_alloc_cnt < work->iov_cnt + need_iov_cnt) { - struct kvec *new; - - work->iov_alloc_cnt += 4; - new = krealloc(work->iov, - sizeof(struct kvec) * work->iov_alloc_cnt, - KSMBD_DEFAULT_GFP | __GFP_ZERO); - if (!new) { - kfree(ar); - work->iov_alloc_cnt -= 4; - return -ENOMEM; - } - work->iov = new; + if (ksmbd_reserve_iov(work, need_iov_cnt)) { + kfree(ar); + return -ENOMEM; } /* Plus rfc_length size on first iov */ diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 0da8cc0972d6..5368430561fb 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -13,6 +13,8 @@ struct ksmbd_conn; struct ksmbd_session; struct ksmbd_tree_connect; +#define KSMBD_WORK_INLINE_IOVS 4 + enum { KSMBD_WORK_ACTIVE = 0, KSMBD_WORK_CANCELLED, @@ -42,6 +44,7 @@ struct ksmbd_work { int iov_alloc_cnt; int iov_cnt; int iov_idx; + struct kvec iov_inline[KSMBD_WORK_INLINE_IOVS]; /* Next cmd hdr in compound req buf*/ int next_smb2_rcv_hdr_off; From 0c054227479ed7e36ebccb3a558bc0ef698264f6 Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Fri, 19 Jun 2026 08:25:46 +0900 Subject: [PATCH 17/50] ksmbd: fix use-after-free of conn->preauth_info in concurrent SMB2 NEGOTIATE conn->preauth_info is shared connection state (struct preauth_integrity_info, kmalloc-96) that is allocated and freed by the SMB2 NEGOTIATE handler and read by the response send path. smb2_handle_negotiate() allocates conn->preauth_info, and on a deassemble_neg_contexts() failure kfrees it and sets it to NULL. Both the allocation and the free/NULL happen under ksmbd_conn_lock(conn) (the connection srv_mutex), which is held across the whole handler body. The response send path smb3_preauth_hash_rsp(), called from the send: block of __handle_ksmbd_work(), reads conn->preauth_info and dereferences conn->preauth_info->Preauth_HashValue (via ksmbd_gen_preauth_integrity_hash()) without taking conn_lock. When a client drives two SMB2 NEGOTIATE requests on the same connection, one worker can free conn->preauth_info on the failing-negotiate path while a concurrent send-path worker is reading it, producing a slab use-after-free read (KASAN-confirmed). The send-path read tested conn->preauth_info for NULL but raced with the free that occurs between the NULL check and the dereference, so the NULL guard alone does not close the window. Serialize the NEGOTIATE-branch read in smb3_preauth_hash_rsp() under ksmbd_conn_lock(conn) and re-check conn->preauth_info inside the lock. Because the negotiate handler holds conn_lock across its kfree + NULL assignment, a reader that also takes conn_lock either runs fully before the allocation or fully after the NULL store, and can never observe the freed-but-not-yet-NULLed pointer. ksmbd_gen_preauth_integrity_hash() takes no locks itself (it only computes a SHA-512 over the buffer), so no lock-ordering inversion is introduced, and conn_lock is a sleepable mutex which is safe on this send path (it already performs network I/O). Fixes: aa7253c2393f ("ksmbd: fix memory leak in smb2_handle_negotiate") Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f1a0bce0c1f7..2bc275ed450a 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9368,10 +9368,13 @@ void smb3_preauth_hash_rsp(struct ksmbd_work *work) WORK_BUFFERS(work, req, rsp); - if (le16_to_cpu(req->Command) == SMB2_NEGOTIATE_HE && - conn->preauth_info) - ksmbd_gen_preauth_integrity_hash(conn, work->response_buf, - conn->preauth_info->Preauth_HashValue); + if (le16_to_cpu(req->Command) == SMB2_NEGOTIATE_HE) { + ksmbd_conn_lock(conn); + if (conn->preauth_info) + ksmbd_gen_preauth_integrity_hash(conn, work->response_buf, + conn->preauth_info->Preauth_HashValue); + ksmbd_conn_unlock(conn); + } if (le16_to_cpu(rsp->Command) == SMB2_SESSION_SETUP_HE && sess) { __u8 *hash_value; From a04159d96c27fd4836538ea6e8ff653b65242eac Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:32:18 +0900 Subject: [PATCH 18/50] ksmbd: handle missing create contexts for lease opens smb2_find_context_vals() assumes that callers only search create contexts when the SMB2 CREATE request contains a non-empty create context area. That is not always true. a client can send RequestedOplockLevel set to SMB2_OPLOCK_LEVEL_LEASE without a lease create context. In that case parse_lease_state() searches for a lease context and smb2_find_context_vals() starts parsing from offset 0 with length 0, returning -EINVAL. This makes the open fail with STATUS_INVALID_PARAMETER. The smbtorture smb2.lease.duplicate_open test hits this while creating a second file without a lease request. Return NULL when the request has no create context area so the missing context is treated the same as any other absent create context. The open then continues without granting a lease. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index d936c338f7c7..5424f2a5cf3d 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1760,6 +1760,9 @@ struct create_context *smb2_find_context_vals(void *open_req, const char *tag, i * CreateContextsOffset and CreateContextsLength are guaranteed to * be valid because of ksmbd_smb2_check_message(). */ + if (!req->CreateContextsOffset || !req->CreateContextsLength) + return NULL; + cc = (struct create_context *)((char *)req + le32_to_cpu(req->CreateContextsOffset)); remain_len = le32_to_cpu(req->CreateContextsLength); From 166e4c07023b9c5d076f69e63164b6e1d52709c9 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:33:03 +0900 Subject: [PATCH 19/50] ksmbd: supersede disconnected delete-on-close durable handle A durable handle opened with FILE_DELETE_ON_CLOSE is preserved across a disconnect so it can be reclaimed by a durable reconnect. smb2.durable-open.delete_on_close2 disconnects such a handle and then reconnects it, expecting the reconnect to succeed. When the client does not reconnect but instead opens the same name with a new delete-on-close create, the preserved handle keeps the file present with delete-on-close set. ksmbd then rejects the new open with STATUS_ACCESS_DENIED on the file_present + FILE_DELETE_ON_CLOSE + OPEN_IF/OVERWRITE_IF path. smb2.durable-open.delete_on_close1 expects this open to create a fresh, empty file instead, i.e. the disconnected handle's delete-on-close must take effect first. Add ksmbd_close_disconnected_durable_delete_on_close(), which closes disconnected (conn == NULL) durable handles that keep a delete-on-close file present. The final close promotes S_DEL_ON_CLS to S_DEL_PENDING and unlinks the file, so a re-resolved path is absent and the new open creates it fresh. Call it from smb2_open() before the delete-on-close conflict check, only for the conflicting open shapes. A live (connected) handle still keeps the file and blocks the open as before. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 17 ++++++++++++ fs/smb/server/vfs_cache.c | 57 +++++++++++++++++++++++++++++++++++++++ fs/smb/server/vfs_cache.h | 1 + 3 files changed, 75 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 2bc275ed450a..37a20c5740cd 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3272,6 +3272,23 @@ int smb2_open(struct ksmbd_work *work) rc = ksmbd_vfs_kern_path(work, name, LOOKUP_NO_SYMLINKS, &path, 1); + + /* + * 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 + * delete-on-close open for the same name arrives instead, the + * disconnected handle must give way: close it so its delete-on-close + * removes the file, then re-resolve so this open can create a fresh one. + */ + if (!rc && (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) && + (req->CreateDisposition == FILE_OVERWRITE_IF_LE || + req->CreateDisposition == FILE_OPEN_IF_LE) && + ksmbd_close_disconnected_durable_delete_on_close(path.dentry)) { + path_put(&path); + rc = ksmbd_vfs_kern_path(work, name, LOOKUP_NO_SYMLINKS, + &path, 1); + } + if (!rc) { file_present = true; diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 39c56942ae44..96fa3f160d5b 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -517,6 +517,63 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) kmem_cache_free(filp_cache, fp); } +/** + * ksmbd_close_disconnected_durable_delete_on_close() - drop a delete-on-close + * file kept present only by disconnected durable handles + * @dentry: dentry of the file being opened + * + * 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 + * (non-reconnect) open arrives for the same name instead, the disconnected + * handle has to give way. Close such handles so their delete-on-close is + * applied and the file is removed once the last handle is gone, letting the + * new open create a fresh file. + * + * The caller's inode reference is dropped before closing so that the final + * close can promote S_DEL_ON_CLS to S_DEL_PENDING and unlink the file. + * + * Return: true if a disconnected durable handle was closed. + */ +bool ksmbd_close_disconnected_durable_delete_on_close(struct dentry *dentry) +{ + struct ksmbd_inode *ci; + struct ksmbd_file *fp, *tmp; + LIST_HEAD(dispose); + bool closed = false; + + ci = ksmbd_inode_lookup_lock(dentry); + if (!ci) + return false; + + down_write(&ci->m_lock); + if (ci->m_flags & (S_DEL_ON_CLS | S_DEL_ON_CLS_STREAM | S_DEL_PENDING)) { + list_for_each_entry_safe(fp, tmp, &ci->m_fp_list, node) { + if (fp->conn || !fp->is_durable || + fp->f_state != FP_INITED) + continue; + list_move_tail(&fp->node, &dispose); + } + } + up_write(&ci->m_lock); + + /* + * Drop our lookup reference before closing so the last __ksmbd_close_fd() + * can drop m_count to zero and unlink the delete-on-close file. The + * collected handles still hold references, so ci stays valid until they + * are closed below. + */ + ksmbd_inode_put(ci); + + while (!list_empty(&dispose)) { + fp = list_first_entry(&dispose, struct ksmbd_file, node); + list_del_init(&fp->node); + __ksmbd_close_fd(NULL, fp); + closed = true; + } + + return closed; +} + static struct ksmbd_file *ksmbd_fp_get(struct ksmbd_file *fp) { if (fp->f_state != FP_INITED) diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index a3a9fda6de91..21c24956c7c2 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -160,6 +160,7 @@ struct ksmbd_file *ksmbd_lookup_fd_slow(struct ksmbd_work *work, u64 id, void ksmbd_fd_put(struct ksmbd_work *work, struct ksmbd_file *fp); struct ksmbd_inode *ksmbd_inode_lookup_lock(struct dentry *d); void ksmbd_inode_put(struct ksmbd_inode *ci); +bool ksmbd_close_disconnected_durable_delete_on_close(struct dentry *dentry); struct ksmbd_file *ksmbd_lookup_global_fd(unsigned long long id); struct ksmbd_file *ksmbd_lookup_durable_fd(unsigned long long id); void ksmbd_put_durable_fd(struct ksmbd_file *fp); From 26fa88dc877ccd2736e9125d37c22e058a4c7b86 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:33:54 +0900 Subject: [PATCH 20/50] ksmbd: invalidate durable handles on oplock break When a durable handle is preserved after a disconnect, its oplock state can still block later opens. If another client opens the same file and the preserved oplock or lease has to be broken, the old durable handle must no longer be reconnectable after the break cannot be acknowledged. ksmbd was treating a missing connection, or an oplock break timeout, as a successful break only by downgrading the oplock state. The old durable handle remained reconnectable, so a later durable reconnect for that stale handle could succeed. The open path can also see a detached durable handle before the break notification helpers fully dispose of it. Invalidate such a preserved durable handle directly when a competing open has to break its batch or exclusive oplock, while leaving ordinary durable reconnects without a competing open untouched. If the old handle still reaches the reconnect path, reject it when the same inode already has another active open. This matches the smb2.durable-open.open2-lease/open2-oplock sequence where a later open replaces the disconnected durable owner and the stale first handle must not be reclaimed. Also, reconnect lookup used only the persistent id. A new durable open can get a persistent id that matches the stale reconnect request after the old durable state is invalidated. Preserve the disconnected handle's old volatile id and require durable reconnect contexts to match it, so a stale reconnect cannot attach to a different durable open. Windows allows the later open to proceed and rejects the old reconnect with STATUS_OBJECT_NAME_NOT_FOUND. The smbtorture smb2.durable-open.oplock test covers this case. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 40 +++++++++++++++++++++++++------ fs/smb/server/smb2pdu.c | 14 +++++++++++ fs/smb/server/vfs_cache.c | 50 ++++++++++++++++++++++++++++++++++++++- fs/smb/server/vfs_cache.h | 4 ++++ 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 5424f2a5cf3d..0fddbaa5ba63 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -676,7 +676,7 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, return m_opinfo; } -static void wait_for_break_ack(struct oplock_info *opinfo) +static bool wait_for_break_ack(struct oplock_info *opinfo) { int rc = 0; @@ -693,7 +693,10 @@ static void wait_for_break_ack(struct oplock_info *opinfo) } opinfo->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->op_state = OPLOCK_STATE_NONE; + return true; } + + return false; } static void wake_up_oplock_break(struct oplock_info *opinfo) @@ -843,7 +846,7 @@ static int smb2_oplock_break_noti(struct oplock_info *opinfo) conn = READ_ONCE(opinfo->conn); if (!conn) - return 0; + return ksmbd_invalidate_durable_fd(opinfo->fid); work = ksmbd_alloc_work_struct(); if (!work) @@ -868,7 +871,8 @@ static int smb2_oplock_break_noti(struct oplock_info *opinfo) INIT_WORK(&work->work, __smb2_oplock_break_noti); ksmbd_queue_work(work); - wait_for_break_ack(opinfo); + if (wait_for_break_ack(opinfo)) + ret = ksmbd_invalidate_durable_fd(opinfo->fid); } else { __smb2_oplock_break_noti(&work->work); if (opinfo->level == SMB2_OPLOCK_LEVEL_II) @@ -950,13 +954,14 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, struct ksmbd_work *work; struct lease_break_info *br_info; struct lease *lease = opinfo->o_lease; + int ret = 0; conn = READ_ONCE(opinfo->conn); if (lease->version == 2 && lease->l_lb && lease->l_lb->conn && !ksmbd_conn_releasing(lease->l_lb->conn)) conn = lease->l_lb->conn; if (!conn) - return 0; + return ksmbd_invalidate_durable_fd(opinfo->fid); work = ksmbd_alloc_work_struct(); if (!work) @@ -987,8 +992,10 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, if (opinfo->op_state == OPLOCK_ACK_WAIT) { INIT_WORK(&work->work, __smb2_lease_break_noti); ksmbd_queue_work(work); - if (wait_ack) - wait_for_break_ack(opinfo); + if (wait_ack) { + if (wait_for_break_ack(opinfo)) + ret = ksmbd_invalidate_durable_fd(opinfo->fid); + } } else { __smb2_lease_break_noti(&work->work); if (opinfo->o_lease->new_state == SMB2_LEASE_NONE_LE) { @@ -996,7 +1003,7 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, lease_update_oplock_levels(opinfo->o_lease); } } - return 0; + return ret; } static void wait_lease_breaking(struct oplock_info *opinfo) @@ -1335,6 +1342,9 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct ksmbd_inode *ci = fp->f_ci; struct lease_table *new_lb = NULL; bool prev_op_has_lease; + bool prev_durable_open = false; + bool prev_durable_detached = false; + unsigned long long prev_fid = KSMBD_NO_FID; bool new_lease = false; __le32 prev_op_state = 0; @@ -1412,7 +1422,17 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, goto op_break_not_needed; } + if (prev_opinfo->o_fp && prev_opinfo->o_fp != fp && + prev_opinfo->o_fp->is_durable) { + prev_durable_open = true; + prev_durable_detached = !prev_opinfo->o_fp->conn || + !prev_opinfo->o_fp->tcon; + prev_fid = prev_opinfo->fid; + } + err = oplock_break(prev_opinfo, break_level, work); + if (prev_durable_detached || (prev_durable_open && err == -ENOENT)) + ksmbd_invalidate_durable_fd(prev_fid); opinfo_put(prev_opinfo); if (err == -ENOENT) goto set_lev; @@ -2029,6 +2049,12 @@ int smb2_check_durable_oplock(struct ksmbd_conn *conn, if (!opinfo) return 0; + if (ksmbd_has_other_active_fd(fp)) { + ksmbd_debug(SMB, "Durable handle reconnect failed: competing open\n"); + ret = -EBADF; + goto out; + } + if (ksmbd_vfs_compare_durable_owner(fp, user) == false) { ksmbd_debug(SMB, "Durable handle reconnect failed: owner mismatch\n"); ret = -EBADF; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 37a20c5740cd..f700f2f94ff2 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2861,6 +2861,13 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } + if (dh_info->fp->durable_volatile_id != + recon_v2->dcontext.Fid.VolatileFileId) { + err = -EBADF; + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + if (memcmp(dh_info->fp->create_guid, recon_v2->dcontext.CreateGuid, SMB2_CREATE_GUID_SIZE)) { err = -EBADF; @@ -2901,6 +2908,13 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } + if (dh_info->fp->durable_volatile_id != + recon->Data.Fid.VolatileFileId) { + err = -EBADF; + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + dh_info->type = dh_idx; dh_info->reconnected = true; ksmbd_debug(SMB, "reconnect Persistent-id from reconnect = %llu\n", diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 96fa3f160d5b..3546d95df76f 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -731,7 +731,8 @@ struct ksmbd_file *ksmbd_lookup_durable_fd(unsigned long long id) struct ksmbd_file *fp; fp = __ksmbd_lookup_fd(&global_ft, id); - if (fp && (fp->conn || + if (fp && (fp->durable_reconnect_disabled || + fp->conn || (fp->durable_scavenger_timeout && (fp->durable_scavenger_timeout < jiffies_to_msecs(jiffies))))) { @@ -750,6 +751,52 @@ void ksmbd_put_durable_fd(struct ksmbd_file *fp) __ksmbd_close_fd(NULL, fp); } +bool ksmbd_has_other_active_fd(struct ksmbd_file *fp) +{ + struct ksmbd_file *lfp; + struct ksmbd_inode *ci = fp->f_ci; + bool ret = false; + + down_read(&ci->m_lock); + list_for_each_entry(lfp, &ci->m_fp_list, node) { + if (lfp == fp) + continue; + + if (lfp->f_state == FP_INITED && + (READ_ONCE(lfp->conn) || READ_ONCE(lfp->tcon))) { + ret = true; + break; + } + } + up_read(&ci->m_lock); + + return ret; +} + +int ksmbd_invalidate_durable_fd(unsigned long long id) +{ + struct ksmbd_file *fp; + + fp = ksmbd_lookup_global_fd(id); + if (!fp) + return -ENOENT; + + fp->durable_reconnect_disabled = true; + + if (fp->conn) { + ksmbd_put_durable_fd(fp); + return -ENOENT; + } + + fp->durable_timeout = 1; + fp->durable_scavenger_timeout = jiffies_to_msecs(jiffies); + ksmbd_put_durable_fd(fp); + if (waitqueue_active(&dh_wq)) + wake_up(&dh_wq); + + return -ENOENT; +} + struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid) { struct ksmbd_file *fp = NULL; @@ -990,6 +1037,7 @@ __close_file_table_ids(struct ksmbd_session *sess, * global_ft. */ idr_remove(ft->idr, id); + fp->durable_volatile_id = fp->volatile_id; fp->volatile_id = KSMBD_NO_FID; write_unlock(&ft->lock); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 21c24956c7c2..4803f41a91ef 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -81,6 +81,7 @@ struct ksmbd_file { struct file *filp; u64 persistent_id; u64 volatile_id; + u64 durable_volatile_id; spinlock_t f_lock; @@ -122,6 +123,7 @@ struct ksmbd_file { bool is_durable; bool is_persistent; bool is_resilient; + bool durable_reconnect_disabled; bool is_posix_ctxt; struct durable_owner owner; @@ -164,6 +166,8 @@ bool ksmbd_close_disconnected_durable_delete_on_close(struct dentry *dentry); struct ksmbd_file *ksmbd_lookup_global_fd(unsigned long long id); struct ksmbd_file *ksmbd_lookup_durable_fd(unsigned long long id); 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); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); From 73cd6295d01d8d49abaa03472878ef133e43d26a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:34:37 +0900 Subject: [PATCH 21/50] ksmbd: fix durable reconnect context parsing SMB2 create context DataLength describes only the create context data payload. It does not include the create context header, name field, or any local padding that exists in ksmbd's helper structures. ksmbd validated durable reconnect contexts by comparing DataOffset + DataLength against sizeof the whole helper structure. This rejects a valid durable v2 reconnect context because the wire DH2C data is 36 bytes while struct create_durable_handle_reconnect_v2 contains an extra four byte pad. Validate the durable context payload length against the corresponding payload member instead. Also keep the reconnect context authoritative when a later durable request context is present, matching the existing durable v1 reconnect behavior. This fixes smbtorture smb2.durable-v2-open.durable-v2-setinfo, where the durable v2 reconnect after SET_INFO was rejected with STATUS_INVALID_PARAMETER. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f700f2f94ff2..35db86da79d3 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2845,9 +2845,8 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (le16_to_cpu(context->DataOffset) + - le32_to_cpu(context->DataLength) < - sizeof(struct create_durable_handle_reconnect_v2)) { + if (le32_to_cpu(context->DataLength) < + sizeof(recon_v2->dcontext)) { err = -EINVAL; goto out; } @@ -2892,9 +2891,8 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (le16_to_cpu(context->DataOffset) + - le32_to_cpu(context->DataLength) < - sizeof(create_durable_reconn_t)) { + if (le32_to_cpu(context->DataLength) < + sizeof(recon->Data)) { err = -EINVAL; goto out; } @@ -2931,9 +2929,8 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (le16_to_cpu(context->DataOffset) + - le32_to_cpu(context->DataLength) < - sizeof(struct create_durable_req_v2)) { + if (le32_to_cpu(context->DataLength) < + sizeof(durable_v2_blob->dcontext)) { err = -EINVAL; goto out; } From 16c30649709d949b439273e61769028b1e706327 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:35:12 +0900 Subject: [PATCH 22/50] ksmbd: handle durable v2 app instance id The SMB2_CREATE_APP_INSTANCE_ID create context is used with durable v2 opens to identify another open from the same application instance. When a new durable v2 open arrives with the same AppInstanceId as an existing open, the server should close the previous open without sending an oplock break notification. ksmbd ignored this create context. A second durable v2 batch oplock open with the same AppInstanceId therefore went through the normal competing open path and sent an oplock break to the first opener. smbtorture smb2.durable-v2-open.app-instance expects no oplock break and then expects the old handle to be closed. Parse and store AppInstanceId for durable v2 opens. Before creating the new open, find an existing file with the same AppInstanceId and close it through the normal close teardown path without issuing an oplock break. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 37 ++++++++++++++++++++ fs/smb/server/vfs_cache.c | 72 +++++++++++++++++++++++++++++++++++++++ fs/smb/server/vfs_cache.h | 1 + 3 files changed, 110 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 35db86da79d3..a21760394637 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2807,8 +2807,10 @@ struct durable_info { unsigned short int type; bool persistent; bool reconnected; + bool app_instance_id; unsigned int timeout; char *CreateGuid; + char AppInstanceId[SMB2_CREATE_GUID_SIZE]; }; static int parse_durable_handle_context(struct ksmbd_work *work, @@ -2993,6 +2995,31 @@ static int parse_durable_handle_context(struct ksmbd_work *work, return err; } +static int parse_app_instance_id(struct smb2_create_req *req, + struct durable_info *dh_info) +{ + struct create_context *context; + char *data; + + context = smb2_find_context_vals(req, SMB2_CREATE_APP_INSTANCE_ID, + SMB2_CREATE_GUID_SIZE); + if (IS_ERR(context)) + return PTR_ERR(context); + if (!context) + return 0; + + if (le32_to_cpu(context->DataLength) < 20) + return -EINVAL; + + data = (char *)context + le16_to_cpu(context->DataOffset); + if (data[0] != 20 || data[1]) + return -EINVAL; + + memcpy(dh_info->AppInstanceId, data + 4, SMB2_CREATE_GUID_SIZE); + dh_info->app_instance_id = true; + return 0; +} + /** * smb2_open() - handler for smb file open request * @work: smb work containing request buffer @@ -3135,6 +3162,9 @@ int smb2_open(struct ksmbd_work *work) ksmbd_debug(SMB, "error parsing durable handle context\n"); goto err_out2; } + rc = parse_app_instance_id(req, &dh_info); + if (rc) + goto err_out2; if (dh_info.reconnected == true) { rc = smb2_check_durable_oplock(conn, share, dh_info.fp, @@ -3161,6 +3191,9 @@ int smb2_open(struct ksmbd_work *work) goto reconnected_fp; } + + if (dh_info.type == DURABLE_REQ_V2 && dh_info.app_instance_id) + ksmbd_close_fd_app_instance_id(dh_info.AppInstanceId); } else if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) { lc = parse_lease_state(req); if (IS_ERR(lc)) { @@ -3775,6 +3808,10 @@ int smb2_open(struct ksmbd_work *work) if (dh_info.type == DURABLE_REQ_V2) { memcpy(fp->create_guid, dh_info.CreateGuid, SMB2_CREATE_GUID_SIZE); + if (dh_info.app_instance_id) + memcpy(fp->app_instance_id, + dh_info.AppInstanceId, + SMB2_CREATE_GUID_SIZE); if (dh_info.timeout) fp->durable_timeout = min_t(unsigned int, dh_info.timeout, diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 3546d95df76f..5a2fddadcddf 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -37,6 +37,8 @@ static struct ksmbd_file_table global_ft; static atomic_long_t fd_limit; static struct kmem_cache *filp_cache; +static int ksmbd_mark_fp_closed(struct ksmbd_file *fp); + #define OPLOCK_NONE 0 #define OPLOCK_EXCLUSIVE 1 #define OPLOCK_BATCH 2 @@ -773,6 +775,76 @@ bool ksmbd_has_other_active_fd(struct ksmbd_file *fp) return ret; } +static struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id) +{ + struct ksmbd_file *fp = NULL; + unsigned int id; + + if (!memchr_inv(app_instance_id, 0, SMB2_CREATE_GUID_SIZE)) + return NULL; + + read_lock(&global_ft.lock); + idr_for_each_entry(global_ft.idr, fp, id) { + if (!memcmp(fp->app_instance_id, app_instance_id, + SMB2_CREATE_GUID_SIZE)) { + fp = ksmbd_fp_get(fp); + break; + } + } + read_unlock(&global_ft.lock); + + return fp; +} + +int ksmbd_close_fd_app_instance_id(char *app_instance_id) +{ + 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; + + opinfo = opinfo_get(fp); + if (!opinfo || !opinfo->sess) + goto out; + + ft = &opinfo->sess->file_table; + write_lock(&ft->lock); + if (fp->f_state == FP_INITED) { + if (has_file_id(fp->volatile_id)) { + idr_remove(ft->idr, fp->volatile_id); + fp->volatile_id = KSMBD_NO_FID; + } + n_to_drop = ksmbd_mark_fp_closed(fp); + } + write_unlock(&ft->lock); + opinfo_put(opinfo); + opinfo = NULL; + + if (!n_to_drop) + goto out; + + down_write(&fp->f_ci->m_lock); + list_del_init(&fp->node); + up_write(&fp->f_ci->m_lock); + + if (atomic_sub_and_test(n_to_drop, &fp->refcount)) { + if (fp->conn) + atomic_dec(&fp->conn->stats.open_files_count); + __ksmbd_close_fd(NULL, fp); + } + return 0; + +out: + if (opinfo) + opinfo_put(opinfo); + ksmbd_put_durable_fd(fp); + return 0; +} + int ksmbd_invalidate_durable_fd(unsigned long long id) { struct ksmbd_file *fp; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 4803f41a91ef..ca391d597e2e 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -168,6 +168,7 @@ struct ksmbd_file *ksmbd_lookup_durable_fd(unsigned long long id); 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); +int ksmbd_close_fd_app_instance_id(char *app_instance_id); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); From 24dcee8305d6ba18c69594ca6fb42eaa32f8e63e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:36:40 +0900 Subject: [PATCH 23/50] ksmbd: preserve open change time across rename inode ctime is updated when a file is renamed. ksmbd returned that ctime directly as SMB2 ChangeTime for handle-based query information. This makes ChangeTime change after a rename through an already-open handle, while Windows keeps the handle's ChangeTime stable for this case. Store the SMB ChangeTime in struct ksmbd_file when the handle is opened and use that value for create, close, and handle-based query information responses. If a client explicitly sets FILE_BASIC_INFORMATION ChangeTime, update the stored value as well. This fixes smbtorture smb2.rename.simple_modtime, which expects ChangeTime and LastWriteTime to remain unchanged after renaming an already-open file. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 23 ++++++++++------------- fs/smb/server/vfs_cache.h | 1 + 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a21760394637..f2ba52c7e325 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3783,6 +3783,7 @@ int smb2_open(struct ksmbd_work *work) fp->create_time = ksmbd_UnixTimeToNT(stat.btime); else fp->create_time = ksmbd_UnixTimeToNT(stat.ctime); + fp->change_time = ksmbd_UnixTimeToNT(stat.ctime); if (req->FileAttributes || fp->f_ci->m_fattr == 0) fp->f_ci->m_fattr = cpu_to_le32(smb2_get_dos_mode(&stat, le32_to_cpu(req->FileAttributes))); @@ -3832,8 +3833,7 @@ int smb2_open(struct ksmbd_work *work) rsp->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); rsp->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - rsp->ChangeTime = cpu_to_le64(time); + rsp->ChangeTime = cpu_to_le64(fp->change_time); rsp->AllocationSize = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.blocks << 9); rsp->EndofFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); @@ -5102,8 +5102,7 @@ static int get_file_basic_info(struct smb2_query_info_rsp *rsp, basic_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); basic_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - basic_info->ChangeTime = cpu_to_le64(time); + basic_info->ChangeTime = cpu_to_le64(fp->change_time); basic_info->Attributes = fp->f_ci->m_fattr; basic_info->Pad = 0; rsp->OutputBufferLength = @@ -5205,8 +5204,7 @@ static int get_file_all_info(struct ksmbd_work *work, file_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); file_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - file_info->ChangeTime = cpu_to_le64(time); + file_info->ChangeTime = cpu_to_le64(fp->change_time); file_info->Attributes = fp->f_ci->m_fattr; file_info->Pad1 = 0; if (ksmbd_stream_fd(fp) == false) { @@ -5415,8 +5413,7 @@ static int get_file_network_open_info(struct smb2_query_info_rsp *rsp, file_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); file_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - file_info->ChangeTime = cpu_to_le64(time); + file_info->ChangeTime = cpu_to_le64(fp->change_time); file_info->Attributes = fp->f_ci->m_fattr; if (ksmbd_stream_fd(fp) == false) { file_info->AllocationSize = cpu_to_le64(stat.blocks << 9); @@ -5547,8 +5544,7 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, file_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); file_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - file_info->ChangeTime = cpu_to_le64(time); + file_info->ChangeTime = cpu_to_le64(fp->change_time); file_info->DosAttributes = fp->f_ci->m_fattr; file_info->Inode = cpu_to_le64(stat.ino); if (ksmbd_stream_fd(fp) == false) { @@ -6271,8 +6267,7 @@ int smb2_close(struct ksmbd_work *work) rsp->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); rsp->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - rsp->ChangeTime = cpu_to_le64(time); + rsp->ChangeTime = cpu_to_le64(fp->change_time); ksmbd_fd_put(work, fp); } else { rsp->Flags = 0; @@ -6485,9 +6480,11 @@ static int set_file_basic_info(struct ksmbd_file *fp, attrs.ia_valid |= (ATTR_ATIME | ATTR_ATIME_SET); } - if (file_info->ChangeTime) + if (file_info->ChangeTime) { + fp->change_time = le64_to_cpu(file_info->ChangeTime); inode_set_ctime_to_ts(inode, ksmbd_NTtimeToUnix(file_info->ChangeTime)); + } if (file_info->LastWriteTime) { attrs.ia_mtime = ksmbd_NTtimeToUnix(file_info->LastWriteTime); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index ca391d597e2e..8c456484cab2 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -97,6 +97,7 @@ struct ksmbd_file { __le32 coption; __le32 cdoption; __u64 create_time; + __u64 change_time; __u64 itime; bool is_nt_open; From 9a5784f4d58b0ad772998329f0309f39b3bc1a09 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:37:22 +0900 Subject: [PATCH 24/50] ksmbd: check parent directory sharing conflicts on rename When renaming a file, some existing opens on the parent directory must block the rename with STATUS_SHARING_VIOLATION. This includes parent directory handles opened with DELETE access and handles opened without FILE_SHARE_DELETE. ksmbd checked only the parent's desired access for FILE_DELETE. That handled smb2.rename.share_delete_and_delete_access, but missed the case where the parent directory was opened without delete access and without delete sharing, so smb2.rename.no_share_delete_no_delete_access incorrectly succeeded. Attribute-only parent opens, however, must not block the rename. smb2.rename.msword opens the parent directory with only SYNCHRONIZE and FILE_READ_ATTRIBUTES, no share access, and then renames an already-open child file. Windows allows this pattern. Reject parent directory handles that request DELETE access, and reject non-attribute-only parent opens that deny FILE_SHARE_DELETE, while allowing attribute-only parent opens to coexist with child rename. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 74b0307cb100..80fd27752b2c 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -702,8 +702,10 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, parent_fp = ksmbd_lookup_fd_inode(old_child->d_parent); if (parent_fp) { - if (parent_fp->daccess & FILE_DELETE_LE) { - pr_err("parent dir is opened with delete access\n"); + if ((parent_fp->daccess & FILE_DELETE_LE) || + (!parent_fp->attrib_only && + !(parent_fp->saccess & FILE_SHARE_DELETE_LE))) { + pr_err("parent dir blocks delete sharing\n"); err = -ESHARE; ksmbd_fd_put(work, parent_fp); goto out3; From c841bd3d8dec33a000d6e31b7e7fafb22c39e4e9 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:37:56 +0900 Subject: [PATCH 25/50] ksmbd: deny renaming directory with open children Windows denies renaming a directory while a file below that directory is still open. smb2.rename.rename_dir_openfile checks this by keeping a file handle open under the directory and then attempting to rename the directory handle. ksmbd did not check open children before calling vfs_rename(), so the rename incorrectly succeeded. For non-POSIX clients, scan the global open file table for active handles whose dentries are below the directory being renamed. If any child is open, fail the rename with -EACCES so the client receives STATUS_ACCESS_DENIED. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs.c | 6 ++++++ fs/smb/server/vfs_cache.c | 25 +++++++++++++++++++++++++ fs/smb/server/vfs_cache.h | 1 + 3 files changed, 32 insertions(+) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 80fd27752b2c..f5fa22d87603 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -700,6 +700,12 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, if (err) goto out_drop_write; + if (!work->tcon->posix_extensions && d_is_dir(old_child) && + ksmbd_has_open_files(old_child)) { + err = -EACCES; + goto out3; + } + parent_fp = ksmbd_lookup_fd_inode(old_child->d_parent); if (parent_fp) { if ((parent_fp->daccess & FILE_DELETE_LE) || diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 5a2fddadcddf..98c5bac93d63 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -10,6 +10,7 @@ #include #include #include +#include #include "glob.h" #include "vfs_cache.h" @@ -914,6 +915,30 @@ struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry) return NULL; } +bool ksmbd_has_open_files(struct dentry *dentry) +{ + struct ksmbd_file *fp; + unsigned int id; + bool ret = false; + + read_lock(&global_ft.lock); + idr_for_each_entry(global_ft.idr, fp, id) { + struct dentry *fp_dentry = fp->filp->f_path.dentry; + + if (fp->f_state != FP_INITED) + continue; + if (fp_dentry == dentry) + continue; + if (is_subdir(fp_dentry, dentry)) { + ret = true; + break; + } + } + read_unlock(&global_ft.lock); + + return ret; +} + #define OPEN_ID_TYPE_VOLATILE_ID (0) #define OPEN_ID_TYPE_PERSISTENT_ID (1) diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 8c456484cab2..52a0e8b1f79f 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -172,6 +172,7 @@ bool ksmbd_has_other_active_fd(struct ksmbd_file *fp); int ksmbd_close_fd_app_instance_id(char *app_instance_id); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); +bool ksmbd_has_open_files(struct dentry *dentry); unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp); void ksmbd_launch_ksmbd_durable_scavenger(void); From 3f67e624e591747c2b2c9c607a76d79f7ffdcabc Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:39:28 +0900 Subject: [PATCH 26/50] ksmbd: propagate failed command status in related compounds In a related compound request, later commands can refer to the file handle from an earlier command using the related FID value. If the earlier command fails without producing a valid compound FID, the later related commands must fail with the same status instead of operating on an invalid or stale handle. smb2.compound.related4 sends CREATE followed by IOCTL, CLOSE and SET_INFO. The CREATE is expected to fail with STATUS_ACCESS_DENIED, and the remaining related commands are expected to return STATUS_ACCESS_DENIED as well. ksmbd only stored the compound FID on successful CREATE and did not remember failed compound statuses. Store the failed status in the work item and make related handle-based requests fail immediately with that status only when the compound FID is invalid. Also preserve and consume the related FID across successful FLUSH, READ and WRITE requests whose responses do not carry a file id. Keep a valid compound FID across non-close failures so later related commands can continue to use the handle. When extracting the FID from a successful READ, WRITE or FLUSH request, use the request structure matching the SMB2 command: READ and WRITE place PersistentFileId and VolatileFileId at a different offset than FLUSH, so a single smb2_flush_req cast can save the wrong value as compound_fid and make the following related request fail with STATUS_FILE_CLOSED (smb2.compound_async.write_write after smb2.compound_async.flush_flush). Only update the saved compound FID when the request carries a valid volatile FID. otherwise an all-ones related FID would overwrite the CREATE FID and break smb2.compound.related6. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/ksmbd_work.h | 1 + fs/smb/server/smb2pdu.c | 159 ++++++++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 5368430561fb..df0554a2c50d 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -60,6 +60,7 @@ struct ksmbd_work { u64 compound_fid; u64 compound_pfid; u64 compound_sid; + __le32 compound_status; const struct cred *saved_cred; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f2ba52c7e325..df79533dc0a2 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -405,6 +405,59 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) 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 { + 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; + } + } 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 + * that fails (e.g. a standalone request with a bad session id) + * must not seed the status for a following related command, + * which has to be evaluated on its own (and may legitimately + * fail with a different status such as INVALID_PARAMETER). The + * compound session id is still tracked so a following related + * command can validate it. + */ + if (req->Flags & SMB2_FLAGS_RELATED_OPERATIONS) + work->compound_status = rsp->Status; } len = get_rfc1002_len(work->response_buf) - work->next_smb2_rsp_hdr_off; @@ -430,6 +483,7 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) ksmbd_debug(SMB, "related flag should be set\n"); work->compound_fid = KSMBD_NO_FID; work->compound_pfid = KSMBD_NO_FID; + work->compound_status = STATUS_SUCCESS; } memset((char *)rsp_hdr, 0, sizeof(struct smb2_hdr) + 2); rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER; @@ -449,6 +503,19 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) memcpy(rsp_hdr->Signature, rcv_hdr->Signature, 16); } +static bool smb2_compound_has_failed(struct ksmbd_work *work, + struct smb2_hdr *rsp) +{ + if (!work->next_smb2_rcv_hdr_off || + has_file_id(work->compound_fid) || + work->compound_status == STATUS_SUCCESS) + return false; + + rsp->Status = work->compound_status; + smb2_set_err_rsp(work); + return true; +} + /** * is_chained_smb2_message() - check for chained command * @work: smb work containing smb request buffer @@ -4608,11 +4675,28 @@ int smb2_query_dir(struct ksmbd_work *work) unsigned char srch_flag; int buffer_sz; struct smb2_query_dir_private query_dir_private = {NULL, }; + unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; ksmbd_debug(SMB, "Received smb2 query directory request\n"); WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + if (ksmbd_override_fsids(work)) { rsp->hdr.Status = STATUS_NO_MEMORY; smb2_set_err_rsp(work); @@ -4625,7 +4709,7 @@ int smb2_query_dir(struct ksmbd_work *work) goto err_out2; } - dir_fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); + dir_fp = ksmbd_lookup_fd_slow(work, id, pid); if (!dir_fp) { rc = -EBADF; goto err_out2; @@ -6088,6 +6172,9 @@ int smb2_query_info(struct ksmbd_work *work) WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + if (ksmbd_override_fsids(work)) { rc = -ENOMEM; goto err_out; @@ -6192,6 +6279,9 @@ int smb2_close(struct ksmbd_work *work) WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + if (test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_PIPE)) { ksmbd_debug(SMB, "IPC pipe close request\n"); @@ -6862,6 +6952,8 @@ int smb2_set_info(struct ksmbd_work *work) if (work->next_smb2_rcv_hdr_off) { req = ksmbd_req_buf_next(work); rsp = ksmbd_resp_buf_next(work); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; if (!has_file_id(req->VolatileFileId)) { ksmbd_debug(SMB, "Compound request set FID = %llu\n", work->compound_fid); @@ -7093,6 +7185,8 @@ int smb2_read(struct ksmbd_work *work) if (work->next_smb2_rcv_hdr_off) { req = ksmbd_req_buf_next(work); rsp = ksmbd_resp_buf_next(work); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; if (!has_file_id(req->VolatileFileId)) { ksmbd_debug(SMB, "Compound request set FID = %llu\n", work->compound_fid); @@ -7366,11 +7460,28 @@ int smb2_write(struct ksmbd_work *work) bool writethrough = false, is_rdma_channel = false; int err = 0; unsigned int max_write_size = work->conn->vals->max_write_size; + unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; ksmbd_debug(SMB, "Received smb2 write request\n"); WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + if (test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_PIPE)) { ksmbd_debug(SMB, "IPC pipe write request\n"); return smb2_write_pipe(work); @@ -7415,7 +7526,7 @@ int smb2_write(struct ksmbd_work *work) goto out; } - fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); + fp = ksmbd_lookup_fd_slow(work, id, pid); if (!fp) { err = -ENOENT; goto out; @@ -7509,13 +7620,30 @@ int smb2_flush(struct ksmbd_work *work) { struct smb2_flush_req *req; struct smb2_flush_rsp *rsp; + u64 id = KSMBD_NO_FID, pid = KSMBD_NO_FID; int err; WORK_BUFFERS(work, req, rsp); ksmbd_debug(SMB, "Received smb2 flush request(fid : %llu)\n", req->VolatileFileId); - err = ksmbd_vfs_fsync(work, req->VolatileFileId, req->PersistentFileId); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + + err = ksmbd_vfs_fsync(work, id, pid); if (err) goto out; @@ -7734,11 +7862,29 @@ int smb2_lock(struct ksmbd_work *work) LIST_HEAD(lock_list); LIST_HEAD(rollback_list); int prior_lock = 0, bkt; + unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; WORK_BUFFERS(work, req, rsp); ksmbd_debug(SMB, "Received smb2 lock request\n"); - fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); + + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + + fp = ksmbd_lookup_fd_slow(work, id, pid); if (!fp) { ksmbd_debug(SMB, "Invalid file id for lock : %llu\n", req->VolatileFileId); err = -ENOENT; @@ -8544,6 +8690,8 @@ int smb2_ioctl(struct ksmbd_work *work) if (work->next_smb2_rcv_hdr_off) { req = ksmbd_req_buf_next(work); rsp = ksmbd_resp_buf_next(work); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; if (!has_file_id(req->VolatileFileId)) { ksmbd_debug(SMB, "Compound request set FID = %llu\n", work->compound_fid); @@ -9208,6 +9356,9 @@ int smb2_notify(struct ksmbd_work *work) WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + if (work->next_smb2_rcv_hdr_off && req->hdr.NextCommand) { rsp->hdr.Status = STATUS_INTERNAL_ERROR; smb2_set_err_rsp(work); From 7d258465ea49d82668f52de96f3f0c84727003e4 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:39:59 +0900 Subject: [PATCH 27/50] ksmbd: validate handle for create or get object id FSCTL_CREATE_OR_GET_OBJECT_ID returned a dummy successful response without checking whether the request handle was valid. That let an invalid related compound handle succeed in smb2.compound.related5, although the client expected STATUS_FILE_CLOSED. Look up the file handle before building the object id response and fail with STATUS_FILE_CLOSED when the handle is invalid or already closed. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index df79533dc0a2..d3bd198ec938 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8789,6 +8789,15 @@ int smb2_ioctl(struct ksmbd_work *work) case FSCTL_CREATE_OR_GET_OBJECT_ID: { struct file_object_buf_type1_ioctl_rsp *obj_buf; + struct ksmbd_file *fp; + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -EBADF; + rsp->hdr.Status = STATUS_FILE_CLOSED; + goto out2; + } + ksmbd_fd_put(work, fp); nbytes = sizeof(struct file_object_buf_type1_ioctl_rsp); obj_buf = (struct file_object_buf_type1_ioctl_rsp *) From e50a07437a9ef5a3b2efe414643e2cdcb6b2e644 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:40:37 +0900 Subject: [PATCH 28/50] ksmbd: preserve compound responses for chained errors set_smb2_rsp_status() resets the response iov and compound offsets before building an error response. That is fine for a single request, but it corrupts a compound response when an error is detected after an earlier compound element has already been completed. smb2.compound.invalid4 sends a READ as the first compound element and a bogus command as the second one. The READ response must remain in the compound response with STATUS_END_OF_FILE, followed by the bogus command response with STATUS_INVALID_PARAMETER. Resetting the response state for the second command breaks the compound framing and the client reports NT_STATUS_INVALID_NETWORK_RESPONSE. When setting an error for a chained command, update and pin only the current compound response slot instead of resetting the whole response. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d3bd198ec938..35f23b427bd1 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -246,6 +246,13 @@ void set_smb2_rsp_status(struct ksmbd_work *work, __le32 err) { struct smb2_hdr *rsp_hdr; + if (work->next_smb2_rcv_hdr_off) { + rsp_hdr = ksmbd_resp_buf_next(work); + rsp_hdr->Status = err; + smb2_set_err_rsp(work); + return; + } + rsp_hdr = smb_get_msg(work->response_buf); rsp_hdr->Status = err; From c5db4de8988f1a621556ca5c4537f77b766ca07d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:41:08 +0900 Subject: [PATCH 29/50] ksmbd: return success for deferred final close ksmbd_close_fd() marks an open file as FP_CLOSED and drops the file table reference. If another in-flight request still holds a reference, the final close is deferred until that request drops its reference. The function currently returns -EINVAL in that deferred-final-close case because fp is cleared when the reference count does not reach zero. That turns a valid close into STATUS_FILE_CLOSED. smb2.compound_find.compound_find_close sends QUERY_DIRECTORY and then closes the same directory handle before receiving the find response. The query holds a reference while it builds the response, so close must mark the handle closed and return success even though final teardown is delayed. Track whether the handle was successfully transitioned to FP_CLOSED and return success when only the final close is deferred. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs_cache.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 98c5bac93d63..b617edef950a 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -640,6 +640,7 @@ int ksmbd_close_fd(struct ksmbd_work *work, u64 id) { struct ksmbd_file *fp; struct ksmbd_file_table *ft; + bool closed = false; if (!has_file_id(id)) return 0; @@ -654,6 +655,7 @@ int ksmbd_close_fd(struct ksmbd_work *work, u64 id) fp = NULL; else { fp->f_state = FP_CLOSED; + closed = true; if (!atomic_dec_and_test(&fp->refcount)) fp = NULL; } @@ -661,7 +663,7 @@ int ksmbd_close_fd(struct ksmbd_work *work, u64 id) write_unlock(&ft->lock); if (!fp) - return -EINVAL; + return closed ? 0 : -EINVAL; __put_fd_final(work, fp); return 0; From affcd98fddc57d81648d8ede8546d31e4a3f8450 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:43:35 +0900 Subject: [PATCH 30/50] ksmbd: send pending interim for last compound I/O smb2.compound_async.write_write and smb2.compound_async.read_read expect the last I/O request in a compound request to become cancellable before its final response is received. smb clients mark a request cancellable after receiving an interim STATUS_PENDING response. ksmbd handled the last READ/WRITE synchronously and returned the final response directly, so the client never observed STATUS_PENDING and req->cancel.can_cancel remained false. For the last READ or WRITE in a compound request, register the work briefly as async and send a STATUS_PENDING interim response before continuing with the normal synchronous completion. The final READ/WRITE response remains unchanged. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 35f23b427bd1..cc9cd9297557 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7176,7 +7176,7 @@ int smb2_read(struct ksmbd_work *work) size_t length, mincount; ssize_t nbytes = 0, remain_bytes = 0; int err = 0; - bool is_rdma_channel = false; + bool is_rdma_channel = false, async_interim = false; unsigned int max_read_size = conn->vals->max_read_size; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; void *aux_payload_buf; @@ -7248,6 +7248,14 @@ int smb2_read(struct ksmbd_work *work) goto out; } + if (work->next_smb2_rcv_hdr_off && !req->hdr.NextCommand) { + err = setup_async_work(work, NULL, NULL); + if (err) + goto out; + smb2_send_interim_resp(work, STATUS_PENDING); + async_interim = true; + } + offset = le64_to_cpu(req->Offset); if (offset < 0) { err = -EINVAL; @@ -7283,6 +7291,8 @@ int smb2_read(struct ksmbd_work *work) kvfree(aux_payload_buf); rsp->hdr.Status = STATUS_END_OF_FILE; smb2_set_err_rsp(work); + if (async_interim) + release_async_work(work); ksmbd_fd_put(work, fp); return -ENODATA; } @@ -7317,6 +7327,8 @@ int smb2_read(struct ksmbd_work *work) kvfree(aux_payload_buf); goto out; } + if (async_interim) + release_async_work(work); /* * RDMA responses are transferred through channel buffers and encrypted * responses use the encryption transform, so only normal SMB transport @@ -7330,6 +7342,8 @@ int smb2_read(struct ksmbd_work *work) return 0; out: + if (async_interim) + release_async_work(work); if (err) { if (err == -EISDIR) rsp->hdr.Status = STATUS_INVALID_DEVICE_REQUEST; @@ -7465,6 +7479,7 @@ int smb2_write(struct ksmbd_work *work) ssize_t nbytes; char *data_buf; bool writethrough = false, is_rdma_channel = false; + bool async_interim = false; int err = 0; unsigned int max_write_size = work->conn->vals->max_write_size; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; @@ -7545,6 +7560,14 @@ int smb2_write(struct ksmbd_work *work) goto out; } + if (work->next_smb2_rcv_hdr_off && !req->hdr.NextCommand) { + err = setup_async_work(work, NULL, NULL); + if (err) + goto out; + smb2_send_interim_resp(work, STATUS_PENDING); + async_interim = true; + } + if (length > max_write_size) { ksmbd_debug(SMB, "limiting write size to max size(%u)\n", max_write_size); @@ -7593,10 +7616,15 @@ int smb2_write(struct ksmbd_work *work) err = ksmbd_iov_pin_rsp(work, rsp, offsetof(struct smb2_write_rsp, Buffer)); if (err) goto out; + if (async_interim) + release_async_work(work); ksmbd_fd_put(work, fp); return 0; out: + if (async_interim) + release_async_work(work); + if (err == -EAGAIN) rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; else if (err == -ENOSPC || err == -EFBIG) From 19043971c947d307c9fc76e8b5e750ce7140b486 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:44:09 +0900 Subject: [PATCH 31/50] ksmbd: honor stream delete sharing for base file smb2.streams.delete opens an alternate data stream without FILE_SHARE_DELETE and then tries to delete the base file. Windows rejects the base-file delete with STATUS_SHARING_VIOLATION while the stream handle is open. ksmbd tracks stream opens on the same ksmbd_inode as the base file, but the delete-on-close path only checked delete access on the base handle before marking the inode delete-pending. As a result, deleting the base file succeeded even though an open stream handle denied delete sharing. Add a helper to detect open stream handles on the same inode that do not allow FILE_SHARE_DELETE, and reject base-file delete pending and DELETE opens with a sharing violation in that case. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 9 +++++++++ fs/smb/server/vfs_cache.c | 27 +++++++++++++++++++++++++++ fs/smb/server/vfs_cache.h | 1 + 3 files changed, 37 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index cc9cd9297557..a3ae37e8b24d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3737,6 +3737,12 @@ int smb2_open(struct ksmbd_work *work) goto err_out; } + if (!stream_name && daccess & FILE_DELETE_LE && + ksmbd_has_stream_without_delete_share(fp)) { + rc = -EPERM; + goto err_out; + } + if (file_present || created) path_put(&path); @@ -6758,6 +6764,9 @@ static int set_file_disposition_info(struct ksmbd_work *work, inode = file_inode(fp->filp); if (file_info->DeletePending) { + if (ksmbd_has_stream_without_delete_share(fp)) + return -ESHARE; + if (S_ISDIR(inode->i_mode) && ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY) return -EBUSY; diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index b617edef950a..11b51320b96e 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -254,6 +254,33 @@ void ksmbd_clear_inode_pending_delete(struct ksmbd_file *fp) up_write(&ci->m_lock); } +bool ksmbd_has_stream_without_delete_share(struct ksmbd_file *fp) +{ + struct ksmbd_file *prev_fp; + struct ksmbd_inode *ci = fp->f_ci; + bool ret = false; + + if (ksmbd_stream_fd(fp)) + return false; + + down_read(&ci->m_lock); + list_for_each_entry(prev_fp, &ci->m_fp_list, node) { + if (prev_fp == fp || !ksmbd_stream_fd(prev_fp)) + continue; + + if (file_inode(fp->filp) != file_inode(prev_fp->filp)) + continue; + + if (!(prev_fp->saccess & FILE_SHARE_DELETE_LE)) { + ret = true; + break; + } + } + up_read(&ci->m_lock); + + return ret; +} + void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, int file_info) { diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 52a0e8b1f79f..8aa87843025f 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -169,6 +169,7 @@ struct ksmbd_file *ksmbd_lookup_durable_fd(unsigned long long id); 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); int ksmbd_close_fd_app_instance_id(char *app_instance_id); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); From 4687da7b28cff019a61d802afac44e2bf92edb98 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:44:40 +0900 Subject: [PATCH 32/50] ksmbd: reject empty-attribute synchronize-only create smb2.create.gentest checks each desired access bit independently and expects an open that requests only SYNCHRONIZE with CreateDisposition OPEN_IF and FileAttributes 0 to fail with STATUS_ACCESS_DENIED. Rejecting all SYNCHRONIZE-only opens is too broad: SYNCHRONIZE does not imply read, write, or delete data access, and smb2.sharemode.sharemode-access expects a SYNCHRONIZE-only open to succeed when it does not conflict with the existing share mode. Limit the rejection to the gentest create shape: SYNCHRONIZE-only access, OPEN_IF disposition, and no file attributes. Other synchronize-only opens are handled by the normal permission and share-mode checks. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a3ae37e8b24d..9a1308f32f45 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3332,6 +3332,13 @@ int smb2_open(struct ksmbd_work *work) goto err_out2; } + if (req->DesiredAccess == FILE_SYNCHRONIZE_LE && + req->CreateDisposition == FILE_OPEN_IF_LE && + !req->FileAttributes) { + rc = -EACCES; + goto err_out2; + } + if (req->FileAttributes && !(req->FileAttributes & FILE_ATTRIBUTE_MASK_LE)) { pr_err("Invalid file attribute : 0x%x\n", le32_to_cpu(req->FileAttributes)); From 0984b1f058fe1f501a04dea2c97897735be1d9f4 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:45:07 +0900 Subject: [PATCH 33/50] ksmbd: tighten create file attribute validation smb2.create.gentest checks each create FileAttributes bit independently and expects FILE_ATTRIBUTE_INTEGRITY_STREAM and FILE_ATTRIBUTE_NO_SCRUB_DATA to be rejected with STATUS_INVALID_PARAMETER. ksmbd validates create FileAttributes against FILE_ATTRIBUTE_MASK, which includes those bits. It also rejects only requests that have no known attribute bit at all, so a request containing both known and unknown bits can pass validation. Use a create-specific attribute mask that excludes INTEGRITY_STREAM and NO_SCRUB_DATA, and reject any bit outside that mask. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 9a1308f32f45..d6001cdce085 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -57,6 +57,10 @@ static void __wbuf(struct ksmbd_work *work, void **req, void **rsp) #define WORK_BUFFERS(w, rq, rs) __wbuf((w), (void **)&(rq), (void **)&(rs)) +#define SMB2_CREATE_FILE_ATTRIBUTE_MASK \ + (FILE_ATTRIBUTE_MASK & ~(FILE_ATTRIBUTE_INTEGRITY_STREAM | \ + FILE_ATTRIBUTE_NO_SCRUB_DATA)) + /** * check_session_id() - check for valid session id in smb header * @conn: connection instance @@ -3339,7 +3343,8 @@ int smb2_open(struct ksmbd_work *work) goto err_out2; } - if (req->FileAttributes && !(req->FileAttributes & FILE_ATTRIBUTE_MASK_LE)) { + if (req->FileAttributes && + (req->FileAttributes & ~cpu_to_le32(SMB2_CREATE_FILE_ATTRIBUTE_MASK))) { pr_err("Invalid file attribute : 0x%x\n", le32_to_cpu(req->FileAttributes)); rc = -EINVAL; From 3b41d8b05dc89b1f3c53ca7f1a084c7c93712ebf Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:47:02 +0900 Subject: [PATCH 34/50] ksmbd: return requested create allocation size smb2.create.blob sends an SMB2_CREATE_ALLOCATION_SIZE create context with a 1MiB allocation size and expects the create response AllocationSize field to match the requested size. smb2.create.open additionally compares the AllocationSize returned in the CREATE response with the AllocationSize returned by FILE_ALL_INFORMATION on the same handle. ksmbd applies the allocation with fallocate(), but then fills both the create response and handle-based information from stat.blocks << 9. On filesystems such as ext4 this can include filesystem allocation rounding and metadata effects, causing a response larger than the SMB2 allocation size context and a disagreement between the two queries. Remember the requested allocation size while processing the create context, store the reported allocation size in struct ksmbd_file, and use it for both the create response and handle-based allocation size responses. Update the stored value when FILE_ALLOCATION_INFORMATION changes it, and fall back to stat.blocks << 9 when no allocation size context was provided. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 34 +++++++++++++++++++++++----------- fs/smb/server/vfs_cache.h | 1 + 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d6001cdce085..68d761690002 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3133,7 +3133,7 @@ int smb2_open(struct ksmbd_work *work) char *stream_name = NULL; bool file_present = false, created = false, already_permitted = false; int share_ret, need_truncate = 0; - u64 time; + u64 time, alloc_size = 0; umode_t posix_mode = 0; __le32 daccess, maximal_access = 0; int iov_len = 0; @@ -3826,7 +3826,6 @@ int smb2_open(struct ksmbd_work *work) rc = PTR_ERR(az_req); goto err_out1; } else if (az_req) { - loff_t alloc_size; int err; if (le16_to_cpu(az_req->ccontext.DataOffset) + @@ -3876,6 +3875,8 @@ int smb2_open(struct ksmbd_work *work) else fp->create_time = ksmbd_UnixTimeToNT(stat.ctime); fp->change_time = ksmbd_UnixTimeToNT(stat.ctime); + fp->allocation_size = S_ISDIR(stat.mode) ? 0 : + (alloc_size ?: stat.blocks << 9); if (req->FileAttributes || fp->f_ci->m_fattr == 0) fp->f_ci->m_fattr = cpu_to_le32(smb2_get_dos_mode(&stat, le32_to_cpu(req->FileAttributes))); @@ -3926,8 +3927,19 @@ int smb2_open(struct ksmbd_work *work) time = ksmbd_UnixTimeToNT(stat.mtime); rsp->LastWriteTime = cpu_to_le64(time); rsp->ChangeTime = cpu_to_le64(fp->change_time); - rsp->AllocationSize = S_ISDIR(stat.mode) ? 0 : - cpu_to_le64(stat.blocks << 9); + /* + * The cached allocation size hides filesystem rounding for the + * requested allocation, but it can go stale when the file grows past + * it via writes (e.g. across a durable reconnect). Refresh it once the + * file exceeds the cached value, rounding the end of file up to the + * volume allocation unit (the filesystem block size, matching the + * SectorsPerAllocationUnit/BytesPerSector ksmbd advertises) rather than + * using the raw on-disk block count, which can include filesystem + * preallocation and metadata rounding. + */ + if (!S_ISDIR(stat.mode) && stat.size > fp->allocation_size) + fp->allocation_size = round_up(stat.size, stat.blksize); + rsp->AllocationSize = cpu_to_le64(fp->allocation_size); rsp->EndofFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); rsp->FileAttributes = fp->f_ci->m_fattr; @@ -5236,7 +5248,7 @@ static int get_file_standard_info(struct smb2_query_info_rsp *rsp, delete_pending = ksmbd_inode_pending_delete(fp); if (ksmbd_stream_fd(fp) == false) { - sinfo->AllocationSize = cpu_to_le64(stat.blocks << 9); + sinfo->AllocationSize = cpu_to_le64(fp->allocation_size); sinfo->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { sinfo->AllocationSize = cpu_to_le64(fp->stream.size); @@ -5317,8 +5329,7 @@ static int get_file_all_info(struct ksmbd_work *work, file_info->Attributes = fp->f_ci->m_fattr; file_info->Pad1 = 0; if (ksmbd_stream_fd(fp) == false) { - file_info->AllocationSize = - cpu_to_le64(stat.blocks << 9); + file_info->AllocationSize = cpu_to_le64(fp->allocation_size); file_info->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { file_info->AllocationSize = cpu_to_le64(fp->stream.size); @@ -5525,7 +5536,7 @@ static int get_file_network_open_info(struct smb2_query_info_rsp *rsp, file_info->ChangeTime = cpu_to_le64(fp->change_time); file_info->Attributes = fp->f_ci->m_fattr; if (ksmbd_stream_fd(fp) == false) { - file_info->AllocationSize = cpu_to_le64(stat.blocks << 9); + file_info->AllocationSize = cpu_to_le64(fp->allocation_size); file_info->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { file_info->AllocationSize = cpu_to_le64(fp->stream.size); @@ -5658,7 +5669,7 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, file_info->Inode = cpu_to_le64(stat.ino); if (ksmbd_stream_fd(fp) == false) { file_info->EndOfFile = cpu_to_le64(stat.size); - file_info->AllocationSize = cpu_to_le64(stat.blocks << 9); + file_info->AllocationSize = cpu_to_le64(fp->allocation_size); } else { file_info->EndOfFile = cpu_to_le64(fp->stream.size); file_info->AllocationSize = cpu_to_le64(fp->stream.size); @@ -6373,8 +6384,7 @@ int smb2_close(struct ksmbd_work *work) } rsp->Flags = SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB; - rsp->AllocationSize = S_ISDIR(stat.mode) ? 0 : - cpu_to_le64(stat.blocks << 9); + rsp->AllocationSize = cpu_to_le64(fp->allocation_size); rsp->EndOfFile = cpu_to_le64(stat.size); rsp->Attributes = fp->f_ci->m_fattr; rsp->CreationTime = cpu_to_le64(fp->create_time); @@ -6707,6 +6717,8 @@ static int set_file_allocation_info(struct ksmbd_work *work, if (size < alloc_blks * 512) i_size_write(inode, size); } + + fp->allocation_size = le64_to_cpu(file_alloc_info->AllocationSize); return 0; } diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 8aa87843025f..f85021c11d6e 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -98,6 +98,7 @@ struct ksmbd_file { __le32 cdoption; __u64 create_time; __u64 change_time; + __u64 allocation_size; __u64 itime; bool is_nt_open; From ba3cf6ee4f0eacc1f8c607b80188e3b32ef5e0e3 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:47:56 +0900 Subject: [PATCH 35/50] ksmbd: apply create security descriptor first smb2.create.aclfile creates files with an SMB2_CREATE_SD_BUFFER create context and expects the resulting security descriptor to match the descriptor supplied by the client. ksmbd currently tries to inherit the parent DACL first and only parses the SMB2_CREATE_SD_BUFFER context when DACL inheritance fails. If inheritance succeeds, the explicit security descriptor supplied on create is ignored. This breaks create requests that include owner/group information in the security descriptor. Apply the create security descriptor first when the context is present. Fall back to the existing inherited/default ACL path only when no create security descriptor was supplied. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 68d761690002..6b84a8ea5b15 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3651,14 +3651,16 @@ int smb2_open(struct ksmbd_work *work) if (posix_acl_rc) ksmbd_debug(SMB, "inherit posix acl failed : %d\n", posix_acl_rc); - if (test_share_config_flag(work->tcon->share_conf, - KSMBD_SHARE_FLAG_ACL_XATTR)) { - rc = smb_inherit_dacl(conn, &path, sess->user->uid, - sess->user->gid); - } + rc = smb2_create_sd_buffer(work, req, &path); + if (rc && rc != -ENOENT) + goto err_out; - if (rc) { - rc = smb2_create_sd_buffer(work, req, &path); + if (rc == -ENOENT) { + if (test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_ACL_XATTR)) { + rc = smb_inherit_dacl(conn, &path, sess->user->uid, + sess->user->gid); + } if (rc) { if (posix_acl_rc) ksmbd_vfs_set_init_posix_acl(idmap, From 7db0da9915fdfd40156d01f20faac08f040fcfa3 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:48:27 +0900 Subject: [PATCH 36/50] ksmbd: downgrade oplock after break timeout smb2.oplock.batch22a opens a file with a batch oplock and then issues a second open that waits for the oplock break timeout. After the timeout the second open should succeed, but the granted oplock level must be level II. When the break times out, oplock_break() returns -ENOENT after invalidating the previous opener. smb_grant_oplock() went straight to set_lev with the original requested oplock level, so the second open could be granted a new batch oplock. Downgrade the requested oplock to level II on the -ENOENT break-timeout path before granting the oplock to the new open. A break that completes because the previous owner closed its handle from the oplock break handler must be distinguished from a real timeout. smb2.oplock.batch7 closes the first handle during the break wait, and the second open is then expected to be granted the originally requested batch oplock. Return -EAGAIN from the non-lease break path when the previous opener closed during the break wait, recheck sharing in smb_grant_oplock(), and grant the requested oplock if the close removed the conflict. Real break timeouts still return -ENOENT and keep the downgrade to level II. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 0fddbaa5ba63..3f35ee7f7d00 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1130,7 +1130,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) - err = -ENOENT; + err = -EAGAIN; wake_up_oplock_break(brk_opinfo); return err; @@ -1434,8 +1434,19 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, if (prev_durable_detached || (prev_durable_open && err == -ENOENT)) ksmbd_invalidate_durable_fd(prev_fid); opinfo_put(prev_opinfo); - if (err == -ENOENT) + if (err == -EAGAIN) { + share_ret = ksmbd_smb_check_shared_mode(fp->filp, fp); + if (share_ret < 0) { + err = share_ret; + goto err_out; + } goto set_lev; + } + if (err == -ENOENT) { + if (req_op_level != SMB2_OPLOCK_LEVEL_NONE) + req_op_level = SMB2_OPLOCK_LEVEL_II; + goto set_lev; + } /* Check all oplock was freed by close */ else if (err < 0) goto err_out; From ec476c2580050ea050c562eeeb508519fc69ea21 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:49:05 +0900 Subject: [PATCH 37/50] ksmbd: avoid level II oplock break notification on unlink smb2_util_unlink() opens the target with FILE_DELETE_ON_CLOSE and then closes that handle. Other clients can also mark a file for delete with SMB2 SET_INFO FileDispositionInformation. When these unlink paths break existing SMB2 level II oplocks, ksmbd sends an unsolicited SMB2_OPLOCK_BREAK notification to none. This races with the synchronous CREATE or SET_INFO response expected by the client, and smbtorture reports NT_STATUS_INVALID_NETWORK_RESPONSE while running smb2.oplock.exclusive2. SMB2 level II oplock breaks do not require an acknowledgment in the delete path. Keep lease handling unchanged, but drop plain SMB2 level II oplocks locally for unlink requests without sending a break notification. Normal write/truncate paths still send the level II to none notification, preserving the behavior covered by smb2.oplock.levelII500. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 31 ++++++++++++++++++++++++------- fs/smb/server/oplock.h | 4 ++++ fs/smb/server/smb2pdu.c | 4 ++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 3f35ee7f7d00..5abeb90ebebb 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1546,7 +1546,7 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work, */ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc, - bool send_interim) + bool send_interim, bool send_oplock_break) { struct oplock_info *op, *brk_op; struct ksmbd_inode *ci; @@ -1593,10 +1593,15 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, SMB2_LEASE_KEY_SIZE)) goto next; brk_op->open_trunc = is_trunc; - oplock_break(brk_op, - brk_op->is_lease && !is_trunc ? - SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE, - send_interim && !sent_interim ? work : NULL); + if (!brk_op->is_lease && !send_oplock_break) { + brk_op->level = SMB2_OPLOCK_LEVEL_NONE; + brk_op->op_state = OPLOCK_STATE_NONE; + } else { + oplock_break(brk_op, + brk_op->is_lease && !is_trunc ? + SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE, + send_interim && !sent_interim ? work : NULL); + } sent_interim = true; next: opinfo_put(brk_op); @@ -1610,7 +1615,19 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc) { - __smb_break_all_levII_oplock(work, fp, is_trunc, true); + __smb_break_all_levII_oplock(work, fp, is_trunc, true, true); +} + +void smb_break_all_levII_oplock_no_interim(struct ksmbd_work *work, + struct ksmbd_file *fp, int is_trunc) +{ + __smb_break_all_levII_oplock(work, fp, is_trunc, false, true); +} + +void smb_break_all_levII_oplock_for_delete(struct ksmbd_work *work, + struct ksmbd_file *fp) +{ + __smb_break_all_levII_oplock(work, fp, 0, false, false); } /** @@ -1627,7 +1644,7 @@ void smb_break_all_oplock(struct ksmbd_work *work, struct ksmbd_file *fp) return; sent_break = smb_break_all_write_oplock(work, fp, 1); - __smb_break_all_levII_oplock(work, fp, 1, !sent_break); + __smb_break_all_levII_oplock(work, fp, 1, !sent_break, true); } /** diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 8d1943586246..3f581d22bb67 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -99,6 +99,10 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, struct lease_ctx_info *lctx, int share_ret); void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc); +void smb_break_all_levII_oplock_no_interim(struct ksmbd_work *work, + struct ksmbd_file *fp, int is_trunc); +void smb_break_all_levII_oplock_for_delete(struct ksmbd_work *work, + struct ksmbd_file *fp); int opinfo_write_to_read(struct oplock_info *opinfo); int opinfo_read_handle_to_read(struct oplock_info *opinfo); int opinfo_write_to_none(struct oplock_info *opinfo); diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 6b84a8ea5b15..d4a40cede7bd 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3809,7 +3809,7 @@ int smb2_open(struct ksmbd_work *work) } if (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) { - smb_break_all_levII_oplock(work, fp, 0); + smb_break_all_levII_oplock_for_delete(work, fp); ksmbd_fd_set_delete_on_close(fp, file_info); } @@ -6796,7 +6796,7 @@ static int set_file_disposition_info(struct ksmbd_work *work, if (S_ISDIR(inode->i_mode) && ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY) return -EBUSY; - smb_break_all_levII_oplock(work, fp, 0); + smb_break_all_levII_oplock_for_delete(work, fp); ksmbd_set_inode_pending_delete(fp); } else { ksmbd_clear_inode_pending_delete(fp); From 854b4f8f80f879aceead300028faeda8c90b4b91 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:49:41 +0900 Subject: [PATCH 38/50] ksmbd: return oplock protocol error for level II ack SMB2 level II to none oplock breaks do not require an acknowledgment from the client. smb2.oplock.levelii500 intentionally acknowledges such a break and expects the server to reject it with STATUS_INVALID_OPLOCK_PROTOCOL. ksmbd drops the local level II oplock to none immediately after sending the break notification because it does not wait for an ACK. When the client then sends the invalid ACK, smb20_oplock_break_ack() sees that the oplock is not in OPLOCK_ACK_WAIT state and returns STATUS_INVALID_DEVICE_STATE before checking the current oplock level. If the oplock is already none when an unexpected SMB2 oplock break ACK arrives, report STATUS_INVALID_OPLOCK_PROTOCOL. Keep the existing STATUS_INVALID_DEVICE_STATE response for other unexpected non-wait states. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d4a40cede7bd..705cef655702 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9225,7 +9225,10 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) if (opinfo->op_state != OPLOCK_ACK_WAIT) { ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", opinfo->op_state); - status = STATUS_INVALID_DEVICE_STATE; + if (opinfo->level == SMB2_OPLOCK_LEVEL_NONE) + status = STATUS_INVALID_OPLOCK_PROTOCOL; + else + status = STATUS_INVALID_DEVICE_STATE; goto err_out; } From 43f21349427dc964bfc7aa450e7e560f292698bb Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:50:12 +0900 Subject: [PATCH 39/50] ksmbd: normalize ungrantable lease states smb2.lease.request verifies which SMB2 lease state combinations are granted by the server. Requests for H-only, W-only, and HW leases are valid lease state bitmasks, but they are not grantable combinations and should be returned as lease state none. ksmbd only checked that the requested bits were inside the SMB2 lease state mask. As a result it could grant H-only, W-only, or HW requests and return non-zero lease states where the client expects no lease. Keep the bitmask validation, but normalize ungrantable combinations to zero before allocating or looking up the lease. The grantable combinations remain unchanged: R, RH, RW, and RHW. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 5abeb90ebebb..331ce10dbb66 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -29,6 +29,17 @@ static bool lease_state_valid(__le32 state) return !(state & ~SMB2_LEASE_STATE_MASK_LE); } +static __le32 lease_state_grantable(__le32 state) +{ + if (state == SMB2_LEASE_READ_CACHING_LE || + state == (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE) || + state == (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_WRITE_CACHING_LE) || + state == SMB2_LEASE_STATE_MASK_LE) + return state; + + return 0; +} + static bool lease_v2_flags_valid(__le32 flags) { return !(flags & ~SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE); @@ -1760,6 +1771,7 @@ struct lease_ctx_info *parse_lease_state(void *open_req) if (!lease_state_valid(lreq->req_state) || !lease_v2_flags_valid(lreq->flags)) goto err_out; + lreq->req_state = lease_state_grantable(lreq->req_state); if (lreq->flags == SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) memcpy(lreq->parent_lease_key, lc->lcontext.ParentLeaseKey, SMB2_LEASE_KEY_SIZE); @@ -1777,6 +1789,7 @@ struct lease_ctx_info *parse_lease_state(void *open_req) lreq->duration = lc->lcontext.LeaseDuration; if (!lease_state_valid(lreq->req_state)) goto err_out; + lreq->req_state = lease_state_grantable(lreq->req_state); lreq->version = 1; } else goto err_out; From b35f8f898cd99d61ac9acbdc87ca3955922e956a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:50:41 +0900 Subject: [PATCH 40/50] ksmbd: break handle caching for share conflicts smb2.lease.break_twice first opens a file with an RHW lease and then tries a second open with restrictive sharing. That open must fail with a sharing violation, but the existing lease should be broken from RHW to RW because only handle caching conflicts with the requested sharing. ksmbd used the normal write-cache break calculation for this path, so RHW was broken to RH. The following successful open then did not generate the expected second break from RW to R. Pass share-conflict context into the lease break helper and, for lease breaks caused by sharing, drop only SMB2_LEASE_HANDLE_CACHING from the current lease state. Other break paths keep the existing write/truncate break behavior. A share-conflict break must also remain a single break. The triggering open fails with a sharing violation and is never granted, so there is no target oplock level to converge on. The lease break retry loop, however, keeps breaking while the lease level is still above req_op_level, which broke RHW all the way down to R in one open (lease_break_info.count became 2 instead of 1). Skip the again loop for share-conflict breaks so the sharing open produces exactly one RHW->RW break and the later successful open produces the separate RW->R break the test expects. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 331ce10dbb66..33e93084c3b5 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1035,7 +1035,7 @@ static void wait_lease_breaking(struct oplock_info *opinfo) } static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, - struct ksmbd_work *in_work) + struct ksmbd_work *in_work, bool share_break) { int err = 0; bool sent_interim = false; @@ -1073,6 +1073,10 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, * none. */ lease->new_state = SMB2_LEASE_NONE_LE; + } else if (share_break && + lease->state & SMB2_LEASE_HANDLE_CACHING_LE) { + lease->new_state = + lease->state & ~SMB2_LEASE_HANDLE_CACHING_LE; } else { if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) { if (lease->state & SMB2_LEASE_HANDLE_CACHING_LE) @@ -1121,7 +1125,13 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (wait_ack) wait_lease_breaking(brk_opinfo); - if (wait_ack && !err && + /* + * A break caused by a share-mode conflict only drops the + * conflicting caching bit and the triggering open still fails + * with a sharing violation, so it must stay a single break. + * Do not cascade down to req_op_level through the again loop. + */ + if (wait_ack && !err && !share_break && lease_break_needed(brk_opinfo, req_op_level, open_trunc)) goto again; @@ -1281,7 +1291,7 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, continue; } - oplock_break(opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL); + oplock_break(opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL, false); opinfo_put(opinfo); } } @@ -1322,7 +1332,7 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp) continue; } - oplock_break(opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL); + oplock_break(opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL, false); opinfo_put(opinfo); } } @@ -1441,7 +1451,8 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, prev_fid = prev_opinfo->fid; } - err = oplock_break(prev_opinfo, break_level, work); + err = oplock_break(prev_opinfo, break_level, work, + share_ret < 0 && prev_opinfo->is_lease); if (prev_durable_detached || (prev_durable_open && err == -ENOENT)) ksmbd_invalidate_durable_fd(prev_fid); opinfo_put(prev_opinfo); @@ -1539,7 +1550,7 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work, } brk_opinfo->open_trunc = is_trunc; - oplock_break(brk_opinfo, SMB2_OPLOCK_LEVEL_II, work); + oplock_break(brk_opinfo, SMB2_OPLOCK_LEVEL_II, work, false); sent_break = true; opinfo_put(brk_opinfo); @@ -1611,7 +1622,8 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, oplock_break(brk_op, brk_op->is_lease && !is_trunc ? SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE, - send_interim && !sent_interim ? work : NULL); + send_interim && !sent_interim ? work : NULL, + false); } sent_interim = true; next: From 889d2e38943ad9ab253dfd7520e6d92867825e7d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:51:35 +0900 Subject: [PATCH 41/50] ksmbd: break conflicting-open leases only as far as needed smb2.lease.oplock and smb2.lease.breaking1 hold a lease and then issue a single conflicting open on the same file. The held lease must break one step to drop write caching (RWH->RH, RW->R) and then stop, so lease_break_info.count is 1 and the lease keeps its read/handle caching. ksmbd instead cascaded the break all the way down to none (e.g. RWH->RH->R->none), so the break count was 2 or 3 and the reported lease state ended at 0. Commit "chain pending lease breaks before waking waiters" forces break_level to SMB2_OPLOCK_LEVEL_NONE for any non-lease open against a handle-caching lease, which drives oplock_break()'s retry loop down to none even when only one open is contending. Drop that break_level override so a conflicting open breaks a lease only to its own compatible level (level II, i.e. RH/R). A deeper break is still required when a truncating open is also waiting behind the same lease break. smb2.lease.breaking3 keeps a normal open pending through RWH->RH and an overwrite open pending behind it, and expects the lease to continue RH->R->none before either open completes. The overwrite waiter sets open_trunc on the lease while it blocks on the pending break, so extend the retry loop to chain another break while that truncating waiter still needs the lease at none. The per-break open_trunc snapshot stays cleared, so the cascade steps down (RH->R->none) instead of collapsing straight to none, and the normal open stays pending until the lease is fully broken. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 33e93084c3b5..afd492be88fb 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1126,13 +1126,22 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (wait_ack) wait_lease_breaking(brk_opinfo); /* - * A break caused by a share-mode conflict only drops the - * conflicting caching bit and the triggering open still fails - * with a sharing violation, so it must stay a single break. - * Do not cascade down to req_op_level through the again loop. + * A share-mode conflict break only drops the conflicting + * caching bit; the triggering open fails with a sharing + * violation, so keep it to a single break. + * + * Otherwise chain another break while the lease is still + * incompatible with this open (req_op_level), or while a + * truncating waiter that arrived during the break still needs + * the lease dropped to none. open_trunc snapshotted for this + * break stays cleared, so the next state is computed from the + * lease state and the cascade steps down (e.g. RH->R->none) + * instead of collapsing straight to none. */ if (wait_ack && !err && !share_break && - lease_break_needed(brk_opinfo, req_op_level, open_trunc)) + (lease_break_needed(brk_opinfo, req_op_level, open_trunc) || + (brk_opinfo->open_trunc && + lease->state != SMB2_LEASE_NONE_LE))) goto again; wake_up_oplock_break(brk_opinfo); @@ -1426,10 +1435,6 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, prev_op_has_lease = prev_opinfo->is_lease; if (prev_op_has_lease) prev_op_state = prev_opinfo->o_lease->state; - if (prev_op_has_lease && !lctx && - prev_op_state & SMB2_LEASE_HANDLE_CACHING_LE) - break_level = SMB2_OPLOCK_LEVEL_NONE; - if (share_ret < 0 && prev_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { err = share_ret; From 256257279c187386ceb18540a1f8e19252fd01f6 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:52:55 +0900 Subject: [PATCH 42/50] ksmbd: validate :: stream type against directory create smb2.streams.dir opens ::$DATA with FILE_DIRECTORY_FILE and expects STATUS_NOT_A_DIRECTORY, then opens ::$DATA without it and expects STATUS_FILE_IS_A_DIRECTORY. Commit "treat unnamed DATA stream as base file" canonicalizes the ::$DATA suffix to a NULL stream name so the open continues through the base-file path. That skipped the stream/directory type validation, which was guarded by "if (stream_name)", so opening a directory's ::$DATA stream with FILE_DIRECTORY_FILE incorrectly returned STATUS_OK and a plain open of it no longer reported STATUS_FILE_IS_A_DIRECTORY. parse_stream_name() still records the explicit $DATA type in s_type even when it clears stream_name. Run the data-stream vs directory validation whenever s_type is DATA_STREAM, not only when stream_name is set, so the canonicalized ::$DATA open is rejected with the correct status. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 705cef655702..c0d3062e4d93 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3453,7 +3453,14 @@ int smb2_open(struct ksmbd_work *work) rc = 0; } - if (stream_name) { + /* + * An explicit ::$DATA suffix names the unnamed data stream and is + * canonicalized to a NULL stream name (base file), but the request + * still has to be validated against the data-stream type, e.g. opening + * ::$DATA with FILE_DIRECTORY_FILE must fail with + * STATUS_NOT_A_DIRECTORY. + */ + if (stream_name || s_type == DATA_STREAM) { if (req->CreateOptions & FILE_DIRECTORY_FILE_LE) { if (s_type == DATA_STREAM) { rc = -EIO; From be939e11c4724d1de3650e8bafd4c3583d9684b2 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:57:55 +0900 Subject: [PATCH 43/50] ksmbd: treat read-control opens as stat opens only for leases A second open that requests only metadata-level access must not break the existing caching state. ksmbd already skips the break for such opens via fp->attrib_only (FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES and FILE_SYNCHRONIZE). An open requesting only READ_CONTROL (reading the security descriptor) must be treated differently depending on the existing caching state. smbtorture smb2.lease.statopen4 expects a read-control open NOT to break a caching lease, while smb2.oplock.statopen1 expects the same open to break a batch oplock. So READ_CONTROL is a stat open for leases but not for oplocks. Extend the stat-open break-skip in smb_grant_oplock() to also cover a read-control-only open, but only when the existing holder is a lease. The global fp->attrib_only flag (used for share-mode, rename and truncate decisions) is left unchanged so oplock behaviour is preserved. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index afd492be88fb..99cbd7aa03fc 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -312,6 +312,18 @@ void opinfo_put(struct oplock_info *opinfo) free_opinfo(opinfo); } +static bool ksmbd_inode_has_lease(struct ksmbd_inode *ci) +{ + struct oplock_info *opinfo = opinfo_get_list(ci); + bool is_lease; + + if (!opinfo) + return false; + is_lease = opinfo->is_lease; + opinfo_put(opinfo); + return is_lease; +} + static void opinfo_add(struct oplock_info *opinfo, struct ksmbd_file *fp) { struct ksmbd_inode *ci = fp->f_ci; @@ -1402,10 +1414,22 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, if (!opinfo_count(fp)) goto set_lev; - /* grant none-oplock if second open is trunc */ - if (fp->attrib_only && fp->cdoption != FILE_OVERWRITE_IF_LE && + /* + * A stat open that only requests metadata access must not break the + * existing caching state. READ_CONTROL (reading the security + * descriptor) does not conflict with a lease, but it does conflict + * with an oplock, so only treat a read-control-only open as a stat + * open when the existing holder is a lease. + */ + if (fp->cdoption != FILE_OVERWRITE_IF_LE && fp->cdoption != FILE_OVERWRITE_LE && - fp->cdoption != FILE_SUPERSEDE_LE) { + fp->cdoption != FILE_SUPERSEDE_LE && + (fp->attrib_only || + (!(fp->daccess & ~(FILE_READ_ATTRIBUTES_LE | + FILE_WRITE_ATTRIBUTES_LE | + FILE_SYNCHRONIZE_LE | + FILE_READ_CONTROL_LE)) && + ksmbd_inode_has_lease(ci)))) { req_op_level = SMB2_OPLOCK_LEVEL_NONE; goto set_lev; } From 6b375be0b4e1be89e9a817880515311503a19114 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:59:06 +0900 Subject: [PATCH 44/50] ksmbd: start file id allocation at 1 ksmbd allocates both the volatile id (per-session file table) and the persistent id (global file table) with idr_alloc_cyclic() starting at 0. The first open after the module loads therefore gets volatile id 0 and persistent id 0, and ksmbd returns an SMB2 FileId of {0, 0} in the create response. Clients treat an all-zero FileId as a null handle. smbtorture's smb2_util_handle_empty() considers {0, 0} empty, so tests that guard the close with it (e.g. smb2.oplock.statopen1, smb2.lease.statopen*) never close that first handle. The leaked open keeps the inode's oplock count non-zero, so a later batch oplock request on the same file is downgraded to level II and the test fails. Start the id allocation at 1 (KSMBD_START_FID) so no handle is ever assigned a {0, 0} FileId, matching the behaviour of other SMB servers. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs_cache.c | 3 ++- fs/smb/server/vfs_cache.h | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 11b51320b96e..7495166b0262 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -992,7 +992,8 @@ static int __open_id(struct ksmbd_file_table *ft, struct ksmbd_file *fp, idr_preload(KSMBD_DEFAULT_GFP); write_lock(&ft->lock); - ret = idr_alloc_cyclic(ft->idr, fp, 0, INT_MAX - 1, GFP_NOWAIT); + ret = idr_alloc_cyclic(ft->idr, fp, KSMBD_START_FID, INT_MAX - 1, + GFP_NOWAIT); if (ret >= 0) { id = ret; ret = 0; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index f85021c11d6e..287f3e675cd3 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -23,7 +23,12 @@ #define FILE_GENERIC_WRITE 0x120116 #define FILE_GENERIC_EXECUTE 0X1200a0 -#define KSMBD_START_FID 0 +/* + * Start volatile/persistent file id allocation at 1. A file id of 0 yields an + * SMB2 FileId of {0, 0}, which clients (e.g. Windows, Samba) treat as a null + * handle and never close, leaking the open on the server. + */ +#define KSMBD_START_FID 1 #define KSMBD_NO_FID (INT_MAX) #define SMB2_NO_FID (0xFFFFFFFFFFFFFFFFULL) From 5a7f4d6d8e7fc9c3b67412f1b8e5b56c9aec21af Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 19:59:56 +0900 Subject: [PATCH 45/50] ksmbd: sleep interruptibly in the durable handle scavenger The durable handle scavenger kthread waits up to DURABLE_HANDLE_MAX_TIMEOUT (300 seconds) between scans using wait_event_timeout(), which sleeps in TASK_UNINTERRUPTIBLE. When there are no durable handles pending expiry the task stays in D state far longer than 120 seconds, so the hung task detector prints a bogus "task ksmbd-durable-s blocked for more than 120 seconds" warning with a backtrace, even though the thread is only idle. Use wait_event_interruptible_timeout() so the thread sleeps in TASK_INTERRUPTIBLE, which the hung task detector ignores. This also suits the already-freezable kthread. Treat a negative return (e.g. -ERESTARTSYS) like a timeout when recomputing the next wake interval. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs_cache.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 7495166b0262..fde22742d193 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1344,10 +1344,10 @@ static int ksmbd_durable_scavenger(void *dummy) if (try_to_freeze()) continue; - remaining_jiffies = wait_event_timeout(dh_wq, + remaining_jiffies = wait_event_interruptible_timeout(dh_wq, ksmbd_durable_scavenger_alive() == false, __msecs_to_jiffies(min_timeout)); - if (remaining_jiffies) + if ((long)remaining_jiffies > 0) min_timeout = jiffies_to_msecs(remaining_jiffies); else min_timeout = DURABLE_HANDLE_MAX_TIMEOUT; From 474fd91f3828a89dd7dc0a862f77f14e9f9240ff Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 21 Jun 2026 21:21:01 +0900 Subject: [PATCH 46/50] ksmbd: fix UBSAN array-index-out-of-bounds in decode_compress_ctxt() decode_compress_ctxt() walks CompressionAlgorithms[] using the client supplied CompressionAlgorithmCount. That field is declared in struct smb2_compression_capabilities_context as a fixed 4-element array, but the number of algorithms is actually variable and clients such as Windows advertise more than four (e.g. LZ77, LZ77+Huffman, LZNT1, Pattern_V1 and LZ4). The on-wire context length is already validated, so the access is within the received buffer, but indexing the statically sized [4] array makes UBSAN report an out-of-bounds access: UBSAN: array-index-out-of-bounds in smb2pdu.c:1122:48 index 4 is out of range for type '__le16 [4]' Call Trace: smb2_handle_negotiate+0xda7/0xde0 [ksmbd] ksmbd_smb_negotiate_common+0x27b/0x3e0 [ksmbd] smb2_negotiate_request+0x14/0x20 [ksmbd] handle_ksmbd_work+0x181/0x500 [ksmbd] Walk the algorithms through a pointer so the fixed-array bounds check is not applied, while keeping the existing length validation that bounds the loop to the data actually received. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index c0d3062e4d93..5859fa68bb84 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1094,6 +1094,7 @@ static __le32 decode_compress_ctxt(struct ksmbd_conn *conn, int ctxt_len) { int alg_cnt, algs_size, i; + __le16 *algs; if (sizeof(struct smb2_neg_context) + 10 > ctxt_len) { pr_err("Invalid SMB2_COMPRESSION_CAPABILITIES context length\n"); @@ -1118,8 +1119,15 @@ static __le32 decode_compress_ctxt(struct ksmbd_conn *conn, return STATUS_INVALID_PARAMETER; } + /* + * CompressionAlgorithms[] is declared as a fixed 4-element array, but + * the actual element count is variable (clients such as Windows may + * advertise more). The on-wire length was validated above, so walk the + * algorithms through a pointer to avoid a fixed-array bounds check. + */ + algs = pneg_ctxt->CompressionAlgorithms; for (i = 0; i < alg_cnt; i++) { - __le16 alg = pneg_ctxt->CompressionAlgorithms[i]; + __le16 alg = algs[i]; /* * LZ77 is the required general-purpose codec. Pattern_V1 is an From 4429b56506f45891d445f4dc4c8a22b3ec9b12de Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 22 Jun 2026 10:16:41 +0900 Subject: [PATCH 47/50] ksmbd: increase SMB3_DEFAULT_TRANS_SIZE from 1MB to 4MB This patch raises `SMB3_DEFAULT_TRANS_SIZE` to 4MB to align it with `smb2 max read/write`. This allows better I/O negotiation with modern clients and improves sequential read/write performance on high-speed networks. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index 3bed676bb5ad..c2512dbcdec8 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -23,7 +23,7 @@ #define MAX_SMB2_HDR_SIZE 0x78 /* 4 len + 64 hdr + (2*24 wct) + 2 bct + 2 pad */ #define SMB21_DEFAULT_IOSIZE (1024 * 1024) -#define SMB3_DEFAULT_TRANS_SIZE (1024 * 1024) +#define SMB3_DEFAULT_TRANS_SIZE (4 * 1024 * 1024) #define SMB3_MIN_IOSIZE (64 * 1024) #define SMB3_MAX_IOSIZE (8 * 1024 * 1024) #define SMB3_MAX_MSGSIZE (4 * 4096) From 954d196bebb2b50151cb96454c72dc113b2af1ac Mon Sep 17 00:00:00 2001 From: Haofeng Li Date: Tue, 23 Jun 2026 09:30:26 +0800 Subject: [PATCH 48/50] ksmbd: validate NTLMv2 response before updating session key ksmbd_auth_ntlmv2() derives the NTLMv2 session key into sess->sess_key before it verifies the NTLMv2 response. ksmbd_decode_ntlmssp_auth_blob() then continues into KEY_XCH even when ksmbd_auth_ntlmv2() failed. With SMB3 multichannel binding, the failed authentication operates on an existing session and the session setup error path does not expire binding sessions. A client can send a binding session setup with a bad NT proof and KEY_XCH and still modify sess->sess_key before STATUS_LOGON_FAILURE is returned. Relevant path: smb2_sess_setup() -> conn->binding = true -> ntlm_authenticate() -> session_user() -> ksmbd_decode_ntlmssp_auth_blob() -> ksmbd_auth_ntlmv2() -> calc_ntlmv2_hash() -> hmac_md5_usingrawkey(..., sess->sess_key) -> crypto_memneq() returns mismatch -> KEY_XCH arc4_crypt(..., sess->sess_key, ...) -> out_err without expiring the binding session Derive the base session key into a local buffer and copy it to sess->sess_key only after the proof matches. Return immediately on authentication failure so KEY_XCH is only processed after successful authentication. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Fixes: f9929ef6a2a5 ("ksmbd: add support for key exchange") Cc: stable@vger.kernel.org Signed-off-by: Haofeng Li Reviewed-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/auth.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index e99409fa721c..86f521e849d5 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -142,6 +142,7 @@ int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess, { char ntlmv2_hash[CIFS_ENCPWD_SIZE]; char ntlmv2_rsp[CIFS_HMAC_MD5_HASH_SIZE]; + char sess_key[SMB2_NTLMV2_SESSKEY_SIZE]; struct hmac_md5_ctx ctx; int rc; @@ -164,12 +165,21 @@ int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess, /* Generate the session key */ hmac_md5_usingrawkey(ntlmv2_hash, CIFS_HMAC_MD5_HASH_SIZE, ntlmv2_rsp, CIFS_HMAC_MD5_HASH_SIZE, - sess->sess_key); + sess_key); if (crypto_memneq(ntlmv2->ntlmv2_hash, ntlmv2_rsp, - CIFS_HMAC_MD5_HASH_SIZE)) - return -EINVAL; - return 0; + CIFS_HMAC_MD5_HASH_SIZE)) { + rc = -EINVAL; + goto out; + } + + memcpy(sess->sess_key, sess_key, sizeof(sess_key)); + rc = 0; +out: + memzero_explicit(ntlmv2_hash, sizeof(ntlmv2_hash)); + memzero_explicit(ntlmv2_rsp, sizeof(ntlmv2_rsp)); + memzero_explicit(sess_key, sizeof(sess_key)); + return rc; } /** @@ -226,6 +236,8 @@ int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob, nt_len - CIFS_ENCPWD_SIZE, domain_name, conn->ntlmssp.cryptkey); kfree(domain_name); + if (ret) + return ret; /* The recovered secondary session key */ if (conn->ntlmssp.client_flags & NTLMSSP_NEGOTIATE_KEY_XCH) { From f455ea21f23e2a82e4adf00d8ee65c268ad82036 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 23 Jun 2026 17:21:29 +0900 Subject: [PATCH 49/50] ksmbd: fix inconsistent indenting warnings Detected by Smatch. fs/smb/server/oplock.c:1446 smb_grant_oplock() warn: inconsistent indenting Reported-by: Dan Carpenter Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 99cbd7aa03fc..673182f243d6 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1443,12 +1443,12 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, if (m_opinfo) { lease_put(opinfo->o_lease); lease_get(m_opinfo->o_lease); - opinfo->o_lease = m_opinfo->o_lease; - opinfo->level = m_opinfo->level; - new_lease = false; - opinfo_put(m_opinfo); - goto out; - } + opinfo->o_lease = m_opinfo->o_lease; + opinfo->level = m_opinfo->level; + new_lease = false; + opinfo_put(m_opinfo); + goto out; + } } prev_opinfo = opinfo_get_list(ci); if (!prev_opinfo || From da793cf6d60233f47ea5e7e9e39425d71dfcdb79 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 24 Jun 2026 05:59:13 +0900 Subject: [PATCH 50/50] ksmbd: fix kernel-doc warnings in smb2_lease_break_noti() kernel test robot report missing kernel-doc descriptions for the 'wait_ack' and 'inc_epoch' parameters of smb2_lease_break_noti(): Warning: fs/smb/server/oplock.c:937 function parameter 'wait_ack' not described in 'smb2_lease_break_noti' Warning: fs/smb/server/oplock.c:937 function parameter 'inc_epoch' not described in 'smb2_lease_break_noti' Document both parameters to silence the warnings. Reported-by: kernel test robot Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 673182f243d6..31dd9f3479b2 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -967,6 +967,8 @@ static void __smb2_lease_break_noti(struct work_struct *wk) * smb2_lease_break_noti() - break lease when a new client request * write lease * @opinfo: contains lease state information + * @wait_ack: wait for lease break acknowledgment from the client + * @inc_epoch: increment the lease epoch before sending the break * * Return: 0 on success, otherwise error */