* FAILED: patch "[PATCH] ksmbd: fix app-instance non-durable supersede session UAF" failed to apply to 7.3-stable tree
@ 2026-11-03 10:44 gregkh
2026-11-03 17:03 ` [PATCH 7.3.y 1/4] ksmbd: use clear_and_wake_up_bit() directly Sasha Levin
0 siblings, 1 reply; 5+ messages in thread
From: gregkh @ 2026-11-03 10:44 UTC (permalink / raw)
To: icb, linkinjeon; +Cc: stable
The patch below does not apply to the 7.3-stable tree.
If someone wants it applied there, or to any other stable or longterm
tree, then please email the backport, including the original git commit
id to <stable@vger.kernel.org>.
To reproduce the conflict and resubmit, you may use the following commands:
git fetch https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/ linux-7.3.y
git checkout FETCH_HEAD
git cherry-pick -x f728cfee6929057087c303d07d7a6a7c61196b48
# <resolve conflicts, build, test, etc.>
git commit -s
git send-email --to '<stable@vger.kernel.org>' --in-reply-to '2026110310-spool-turban-3d3a@gregkh' --subject-prefix 'PATCH 7.3.y' 'HEAD^..'
Possible dependencies:
thanks,
greg k-h
------------------ original commit in Linus's tree ------------------
From f728cfee6929057087c303d07d7a6a7c61196b48 Mon Sep 17 00:00:00 2001
From: Ian Bridges <icb@fastmail.org>
Date: Mon, 26 Oct 2026 00:34:22 -0500
Subject: [PATCH] ksmbd: fix app-instance non-durable supersede session UAF
ksmbd_close_fd_app_instance_id() supersedes a prior open that has the
same AppInstanceId. It reaches that open's session through opinfo->sess.
This pointer is not refcounted.
Session teardown detaches durable handles before it frees the session.
It does not detach a non-durable open. That open still points at the
session through opinfo->sess after the session is freed. Another session
can supersede the open at the same time. The supersede then takes a
write lock through the stale pointer, in the freed session's file table.
BUG: KASAN: slab-use-after-free in _raw_write_lock+0x71/0xd0
Call Trace:
_raw_write_lock+0x71/0xd0
ksmbd_close_fd_app_instance_id+0xd9/0x670 [ksmbd]
smb2_open+0x3e10/0xd910 [ksmbd]
Freed by task 212:
ksmbd_session_destroy+0x38d/0x480 [ksmbd]
Fix this by looking the session up by opinfo->session_id. The lookup
returns a reference, as smb2_break_collect_channels() does. Recheck that
opinfo->sess still points at it before using it.
Do the lookup without m_lock held. destroy_previous_session() takes
m_lock under the session table lock, and taking them in the other order
here would deadlock.
Fixes: 16c30649709d ("ksmbd: handle durable v2 app instance id")
Cc: stable@vger.kernel.org # e0386cd6498b: ksmbd: validate AppInstanceId takeover target
Signed-off-by: Ian Bridges <icb@fastmail.org>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index 7a0f13277ec0..4368e069fd5e 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -1262,7 +1262,9 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
struct ksmbd_file *fp, bool *closed)
{
struct ksmbd_file_table *ft;
+ struct ksmbd_session *sess;
struct oplock_info *opinfo;
+ u64 session_id;
int n_to_drop = 0;
*closed = false;
@@ -1275,10 +1277,35 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
up_read(&fp->f_ci->m_lock);
goto out;
}
+ /*
+ * opinfo->sess is not refcounted, and m_lock does not keep it alive.
+ * The owner can be torn down while we hold only a reference to fp.
+ * Look the owner up by id to take a real reference instead.
+ */
+ session_id = opinfo->session_id;
+ up_read(&fp->f_ci->m_lock);
- ft = &opinfo->sess->file_table;
+ /* destroy_previous_session() takes m_lock under sessions_table_lock. */
+ sess = ksmbd_session_lookup_slowpath(session_id);
+ if (!sess)
+ goto out;
+
+ down_read(&fp->f_ci->m_lock);
+ if (opinfo->sess != sess) {
+ up_read(&fp->f_ci->m_lock);
+ ksmbd_user_session_put(sess);
+ goto out;
+ }
+ ft = &sess->file_table;
write_lock(&ft->lock);
- if (fp->f_state == FP_INITED && has_file_id(fp->volatile_id) &&
+ /*
+ * The table may already be detached by destroy_previous_session().
+ * A new session could also reuse the old id and address and pass the
+ * check above. Only remove fp if this table still publishes it.
+ */
+ if (ft->idr && fp->f_state == FP_INITED &&
+ has_file_id(fp->volatile_id) &&
+ idr_find(ft->idr, fp->volatile_id) == fp &&
ksmbd_app_instance_id_matches(work, fp)) {
idr_remove(ft->idr, fp->volatile_id);
fp->volatile_id = KSMBD_NO_FID;
@@ -1286,6 +1313,7 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
}
write_unlock(&ft->lock);
up_read(&fp->f_ci->m_lock);
+ ksmbd_user_session_put(sess);
opinfo_put(opinfo);
opinfo = NULL;
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 7.3.y 1/4] ksmbd: use clear_and_wake_up_bit() directly
2026-11-03 10:44 FAILED: patch "[PATCH] ksmbd: fix app-instance non-durable supersede session UAF" failed to apply to 7.3-stable tree gregkh
@ 2026-11-03 17:03 ` Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 2/4] ksmbd: track delivery of queued TCP writes Sasha Levin
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Sasha Levin @ 2026-11-03 17:03 UTC (permalink / raw)
To: stable; +Cc: Daijiamu, ChenXiaoSong, ChenXiaoSong, Namjae Jeon, Sasha Levin
From: Daijiamu <3058704216@qq.com>
[ Upstream commit 8852052e32df965a76daecbd8cda0ecf950b54df ]
clear_and_wake_up_bit() was introduced by commit 8236b0ae31c8 ("bdi:
wake up concurrent wb_shutdown() callers.") to replace the open-coded
clear_bit_unlock() + smp_mb__after_atomic() + wake_up_bit() sequence.
Replace the sequence and drop the now-trivial wake_up_oplock_break()
wrapper, calling the helper directly at each call site.
Suggested-by: ChenXiaoSong <chenxiaosong@chenxiaosong.com>
Signed-off-by: Daijiamu <3058704216@qq.com>
Reviewed-by: ChenXiaoSong <chenxiaosong@kylinos.cn>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
Stable-dep-of: f728cfee6929 ("ksmbd: fix app-instance non-durable supersede session UAF")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/smb/server/oplock.c | 20 ++++++--------------
1 file changed, 6 insertions(+), 14 deletions(-)
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 1b8c3482d1e4e..25ff8408f0ba2 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -763,14 +763,6 @@ static bool wait_for_break_ack(struct oplock_info *opinfo)
return false;
}
-static void wake_up_oplock_break(struct oplock_info *opinfo)
-{
- clear_bit_unlock(0, &opinfo->pending_break);
- /* memory barrier is needed for wake_up_bit() */
- smp_mb__after_atomic();
- wake_up_bit(&opinfo->pending_break, 0);
-}
-
static bool oplock_break_set_ack_wait(struct oplock_info *opinfo)
{
bool ret = false;
@@ -826,13 +818,13 @@ static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level)
if (opinfo->level <= req_op_level) {
if (opinfo->is_lease == false) {
- wake_up_oplock_break(opinfo);
+ clear_and_wake_up_bit(0, &opinfo->pending_break);
return 1;
}
if (opinfo->o_lease->state !=
(SMB2_LEASE_HANDLE_CACHING_LE |
SMB2_LEASE_READ_CACHING_LE)) {
- wake_up_oplock_break(opinfo);
+ clear_and_wake_up_bit(0, &opinfo->pending_break);
return 1;
}
}
@@ -1260,7 +1252,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
SMB2_LEASE_HANDLE_CACHING_LE)) {
if (!oplock_break_set_ack_wait(brk_opinfo)) {
atomic_dec_if_positive(&brk_opinfo->breaking_cnt);
- wake_up_oplock_break(brk_opinfo);
+ clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return -ENOENT;
}
} else
@@ -1309,7 +1301,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
lease->state != SMB2_LEASE_NONE_LE)))
goto again;
- wake_up_oplock_break(brk_opinfo);
+ clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return err;
} else {
err = oplock_break_pending(brk_opinfo, req_op_level);
@@ -1319,7 +1311,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH ||
brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) {
if (!oplock_break_set_ack_wait(brk_opinfo)) {
- wake_up_oplock_break(brk_opinfo);
+ clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return -ENOENT;
}
}
@@ -1342,7 +1334,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level);
if (brk_opinfo->op_state == OPLOCK_CLOSING)
err = -EAGAIN;
- wake_up_oplock_break(brk_opinfo);
+ clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return err;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 7.3.y 2/4] ksmbd: track delivery of queued TCP writes
2026-11-03 17:03 ` [PATCH 7.3.y 1/4] ksmbd: use clear_and_wake_up_bit() directly Sasha Levin
@ 2026-11-03 17:03 ` Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 3/4] ksmbd: retry break notifications on another connection Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 4/4] ksmbd: fix app-instance non-durable supersede session UAF Sasha Levin
2 siblings, 0 replies; 5+ messages in thread
From: Sasha Levin @ 2026-11-03 17:03 UTC (permalink / raw)
To: stable; +Cc: Namjae Jeon, Pan Guihua, Sasha Levin
From: Namjae Jeon <linkinjeon@kernel.org>
[ Upstream commit c0632d2010ffc006eed9becc8d0d10d54f294bbd ]
A nonblocking break send must distinguish a PDU accepted by TCP from one
still queued in ksmbd. Retrying an accepted PDU can send duplicates, while
abandoning a partial send can corrupt the stream.
Queue owned PDU bytes when TCP cannot accept the full message. Track the
stream position through socket delivery and TCP ACK, and expose it to break
retries. Bound retained data and stop queued writes during teardown.
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
Stable-dep-of: f728cfee6929 ("ksmbd: fix app-instance non-durable supersede session UAF")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/smb/server/connection.c | 52 +++++-
fs/smb/server/connection.h | 24 ++-
fs/smb/server/transport_tcp.c | 299 +++++++++++++++++++++++++++++++++-
3 files changed, 356 insertions(+), 19 deletions(-)
diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c
index 52e2892c58b4d..b519376ecc17b 100644
--- a/fs/smb/server/connection.c
+++ b/fs/smb/server/connection.c
@@ -458,17 +458,13 @@ void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status)
void ksmbd_conn_abort(struct ksmbd_conn *conn)
{
- bool shutdown = false;
-
spin_lock(&conn->request_lock);
- if (!ksmbd_conn_exiting(conn) && !ksmbd_conn_releasing(conn)) {
+ if (!ksmbd_conn_exiting(conn) && !ksmbd_conn_releasing(conn))
ksmbd_conn_set_exiting(conn);
- shutdown = true;
- }
spin_unlock(&conn->request_lock);
wake_up_all(&conn->req_running_q);
- if (shutdown && conn->transport->ops->shutdown)
+ if (conn->transport->ops->shutdown)
conn->transport->ops->shutdown(conn->transport);
}
@@ -535,12 +531,19 @@ static int __ksmbd_conn_write(struct ksmbd_work *work,
tx->need_invalidate_rkey = work->need_invalidate_rkey;
tx->remote_key = work->remote_key;
- ksmbd_conn_lock(conn);
+ if (tx->tracking) {
+ /* A busy writer must not consume the break's retry budget. */
+ if (!mutex_trylock(&conn->srv_mutex))
+ return -EAGAIN;
+ } else {
+ ksmbd_conn_lock(conn);
+ }
sent = conn->transport->ops->writev(conn->transport, tx);
ksmbd_conn_unlock(conn);
if (sent < 0) {
- pr_err("Failed to send message: %d\n", sent);
+ if (!tx->tracking || sent != -EAGAIN)
+ pr_err("Failed to send message: %d\n", sent);
return sent;
}
@@ -554,6 +557,20 @@ int ksmbd_conn_write(struct ksmbd_work *work)
return __ksmbd_conn_write(work, &tx);
}
+int ksmbd_conn_write_tracked(struct ksmbd_work *work,
+ struct ksmbd_tx_tracking *tracking)
+{
+ struct ksmbd_transport_write tx = {};
+
+ tracking->valid = false;
+ /* Transports without delivery tracking keep their synchronous send path. */
+ if (work->conn->transport->ops->is_acked) {
+ tx.msg_flags = MSG_DONTWAIT;
+ tx.tracking = tracking;
+ }
+ return __ksmbd_conn_write(work, &tx);
+}
+
int ksmbd_conn_write_eor(struct ksmbd_work *work)
{
struct ksmbd_transport_write tx = {
@@ -807,7 +824,12 @@ int ksmbd_conn_handler_loop(void *p)
}
}
+ /* Exclude new break senders before draining their work references. */
+ spin_lock(&conn->request_lock);
ksmbd_conn_set_releasing(conn);
+ spin_unlock(&conn->request_lock);
+ /* TCP writers waiting for queue space must release their work references. */
+ wake_up_all(&conn->req_running_q);
ksmbd_conn_cancel_async_requests(conn);
/* Wait till all reference dropped to the Server object*/
ksmbd_debug(CONN, "Wait for all pending requests(%d)\n", atomic_read(&conn->r_count));
@@ -834,6 +856,20 @@ void ksmbd_conn_r_count_inc(struct ksmbd_conn *conn)
atomic_inc(&conn->r_count);
}
+/* The caller already owns a connection reference. */
+bool ksmbd_conn_r_count_inc_if_good(struct ksmbd_conn *conn)
+{
+ bool ret = false;
+
+ spin_lock(&conn->request_lock);
+ if (ksmbd_conn_good(conn)) {
+ atomic_inc(&conn->r_count);
+ ret = true;
+ }
+ spin_unlock(&conn->request_lock);
+ return ret;
+}
+
void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn)
{
/*
diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h
index 8671ca6fe1267..a4f5a64b36b4e 100644
--- a/fs/smb/server/connection.h
+++ b/fs/smb/server/connection.h
@@ -102,6 +102,8 @@ struct ksmbd_conn {
wait_queue_head_t r_count_q;
/* Lock to protect requests list*/
spinlock_t request_lock;
+ /* Set under request_lock at teardown start and kept set thereafter. */
+ bool releasing;
struct list_head requests;
struct list_head async_requests;
int connection_type;
@@ -161,6 +163,13 @@ struct ksmbd_conn_ops {
int (*terminate_fn)(struct ksmbd_conn *conn);
};
+struct ksmbd_tx_tracking {
+ /* Required TCP byte count, including transport and socket queues. */
+ u64 end;
+ unsigned long retry_after;
+ bool valid;
+};
+
struct ksmbd_transport_write {
struct kvec *iov;
int iov_cnt;
@@ -168,6 +177,7 @@ struct ksmbd_transport_write {
bool need_invalidate_rkey;
unsigned int remote_key;
int msg_flags;
+ struct ksmbd_tx_tracking *tracking;
};
struct ksmbd_transport_ops {
@@ -175,8 +185,12 @@ struct ksmbd_transport_ops {
void (*shutdown)(struct ksmbd_transport *t);
int (*read)(struct ksmbd_transport *t, char *buf,
unsigned int size, int max_retries);
+ /* Retained bytes must be privately owned before a full result is returned. */
int (*writev)(struct ksmbd_transport *t,
const struct ksmbd_transport_write *tx);
+ /* Optional delivery tracking for nonblocking break sends. */
+ bool (*is_acked)(struct ksmbd_transport *t,
+ const struct ksmbd_tx_tracking *tracking);
int (*rdma_read)(struct ksmbd_transport *t,
void *buf, unsigned int len,
struct smbdirect_buffer_descriptor_v1 *desc,
@@ -216,6 +230,8 @@ int ksmbd_conn_wq_init(void);
void ksmbd_conn_wq_destroy(void);
bool ksmbd_conn_lookup_dialect(struct ksmbd_conn *c);
int ksmbd_conn_write(struct ksmbd_work *work);
+int ksmbd_conn_write_tracked(struct ksmbd_work *work,
+ struct ksmbd_tx_tracking *tracking);
int ksmbd_conn_write_eor(struct ksmbd_work *work);
int ksmbd_conn_rdma_read(struct ksmbd_conn *conn,
void *buf, unsigned int buflen,
@@ -234,6 +250,7 @@ void ksmbd_conn_transport_destroy(void);
void ksmbd_conn_lock(struct ksmbd_conn *conn);
void ksmbd_conn_unlock(struct ksmbd_conn *conn);
void ksmbd_conn_r_count_inc(struct ksmbd_conn *conn);
+bool ksmbd_conn_r_count_inc_if_good(struct ksmbd_conn *conn);
void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn);
/*
@@ -249,7 +266,8 @@ static inline bool ksmbd_conn_new(struct ksmbd_conn *conn)
static inline bool ksmbd_conn_good(struct ksmbd_conn *conn)
{
- return READ_ONCE(conn->status) == KSMBD_SESS_GOOD;
+ return !READ_ONCE(conn->releasing) &&
+ READ_ONCE(conn->status) == KSMBD_SESS_GOOD;
}
static inline unsigned int
@@ -283,7 +301,8 @@ static inline bool ksmbd_conn_exiting(struct ksmbd_conn *conn)
static inline bool ksmbd_conn_releasing(struct ksmbd_conn *conn)
{
- return READ_ONCE(conn->status) == KSMBD_SESS_RELEASING;
+ return READ_ONCE(conn->releasing) ||
+ READ_ONCE(conn->status) == KSMBD_SESS_RELEASING;
}
static inline void ksmbd_conn_set_new(struct ksmbd_conn *conn)
@@ -318,6 +337,7 @@ static inline void ksmbd_conn_set_exiting(struct ksmbd_conn *conn)
static inline void ksmbd_conn_set_releasing(struct ksmbd_conn *conn)
{
+ WRITE_ONCE(conn->releasing, true);
WRITE_ONCE(conn->status, KSMBD_SESS_RELEASING);
}
diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c
index b2a3d8b4f58e6..d3de4a1cc7d60 100644
--- a/fs/smb/server/transport_tcp.c
+++ b/fs/smb/server/transport_tcp.c
@@ -5,6 +5,7 @@
*/
#include <linux/freezer.h>
+#include <linux/uio.h>
#include "smb_common.h"
#include "server.h"
@@ -29,11 +30,30 @@ static LIST_HEAD(iface_list);
static int bind_additional_ifaces;
+struct tcp_tx {
+ struct list_head list;
+ size_t len;
+ size_t offset;
+ unsigned long deadline;
+ int msg_flags;
+ u8 data[];
+};
+
struct tcp_transport {
struct ksmbd_transport transport;
struct socket *sock;
struct kvec *iov;
unsigned int nr_iov;
+ /* Serializes queue access and socket writes. Never taken by callbacks. */
+ struct mutex tx_lock;
+ struct list_head tx_queue;
+ struct delayed_work tx_work;
+ size_t tx_queued;
+ /* TCP byte positions for accepted PDUs and bytes passed to the socket. */
+ u64 tx_end;
+ u64 tx_sent;
+ bool tx_stopped;
+ void (*write_space)(struct sock *sk);
};
static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops;
@@ -42,6 +62,10 @@ static void tcp_stop_kthread(struct task_struct *kthread);
static void ksmbd_tcp_stop_listener(struct interface *iface);
static struct interface *alloc_iface(char *ifname);
static void ksmbd_tcp_disconnect(struct ksmbd_transport *t);
+static void ksmbd_tcp_shutdown(struct ksmbd_transport *t);
+static void ksmbd_tcp_tx_work(struct work_struct *work);
+static void ksmbd_tcp_write_space(struct sock *sk);
+static void ksmbd_tcp_stop_tx(struct tcp_transport *t);
#define KSMBD_TRANS(t) (&(t)->transport)
#define TCP_TRANS(t) ((struct tcp_transport *)container_of(t, \
@@ -66,6 +90,9 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk)
if (!t)
return NULL;
t->sock = client_sk;
+ mutex_init(&t->tx_lock);
+ INIT_LIST_HEAD(&t->tx_queue);
+ INIT_DELAYED_WORK(&t->tx_work, ksmbd_tcp_tx_work);
conn = ksmbd_conn_alloc();
if (!conn) {
@@ -85,13 +112,26 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk)
conn->inet_addr = inet_sk(client_sk->sk)->inet_daddr;
conn->inet_hash = ipv4_addr_hash(inet_sk(client_sk->sk)->inet_daddr);
#endif
- down_write(&conn_list_lock);
- hash_add(conn_list, &conn->hlist, conn->inet_hash);
- up_write(&conn_list_lock);
-
conn->transport = KSMBD_TRANS(t);
KSMBD_TRANS(t)->conn = conn;
KSMBD_TRANS(t)->ops = &ksmbd_tcp_transport_ops;
+
+ lock_sock(client_sk->sk);
+ t->tx_end = tcp_sk(client_sk->sk)->bytes_acked +
+ (u32)(tcp_sk(client_sk->sk)->write_seq -
+ tcp_sk(client_sk->sk)->snd_una);
+ t->tx_sent = t->tx_end;
+ release_sock(client_sk->sk);
+
+ write_lock_bh(&client_sk->sk->sk_callback_lock);
+ t->write_space = client_sk->sk->sk_write_space;
+ client_sk->sk->sk_user_data = t;
+ client_sk->sk->sk_write_space = ksmbd_tcp_write_space;
+ write_unlock_bh(&client_sk->sk->sk_callback_lock);
+
+ down_write(&conn_list_lock);
+ hash_add(conn_list, &conn->hlist, conn->inet_hash);
+ up_write(&conn_list_lock);
return t;
}
@@ -99,6 +139,7 @@ static void ksmbd_tcp_free_transport(struct ksmbd_transport *kt)
{
struct tcp_transport *t = TCP_TRANS(kt);
+ ksmbd_tcp_stop_tx(t);
sock_release(t->sock);
kfree(t->iov);
kfree(t);
@@ -106,7 +147,8 @@ static void ksmbd_tcp_free_transport(struct ksmbd_transport *kt)
static void free_transport(struct tcp_transport *t)
{
- kernel_sock_shutdown(t->sock, SHUT_RDWR);
+ ksmbd_tcp_shutdown(KSMBD_TRANS(t));
+ ksmbd_tcp_stop_tx(t);
ksmbd_conn_free(KSMBD_TRANS(t)->conn);
}
@@ -431,15 +473,251 @@ static int ksmbd_tcp_read(struct ksmbd_transport *t, char *buf,
return ksmbd_tcp_readv(TCP_TRANS(t), &iov, 1, to_read, max_retries);
}
+static bool ksmbd_tcp_tx_has_space(struct tcp_transport *t, size_t size)
+{
+ size_t limit = max_t(size_t, READ_ONCE(t->sock->sk->sk_sndbuf), size);
+
+ /* Allow one large response, but bound the retained response buffers. */
+ return READ_ONCE(t->tx_queued) <= limit - size;
+}
+
+static struct tcp_tx *ksmbd_tcp_alloc_tx(const struct ksmbd_transport_write *tx,
+ size_t offset)
+{
+ struct tcp_tx *msg;
+ struct iov_iter iter;
+ size_t remaining = tx->size - offset;
+
+ msg = kvmalloc(struct_size(msg, data, remaining), KSMBD_DEFAULT_GFP);
+ if (!msg)
+ return NULL;
+
+ iov_iter_kvec(&iter, ITER_SOURCE, tx->iov, tx->iov_cnt, tx->size);
+ iov_iter_advance(&iter, offset);
+ if (!copy_from_iter_full(msg->data, remaining, &iter)) {
+ kvfree(msg);
+ return NULL;
+ }
+ msg->len = remaining;
+ msg->offset = 0;
+ msg->msg_flags = tx->msg_flags;
+ return msg;
+}
+
+static void ksmbd_tcp_purge_tx(struct tcp_transport *t)
+{
+ struct tcp_tx *msg, *next;
+
+ list_for_each_entry_safe(msg, next, &t->tx_queue, list) {
+ list_del(&msg->list);
+ kvfree(msg);
+ }
+ WRITE_ONCE(t->tx_queued, 0);
+}
+
+static bool ksmbd_tcp_send_retry(int ret)
+{
+ return ret == -EAGAIN || ret == -EINTR || ret == -ERESTARTSYS ||
+ ret == -ENOMEM;
+}
+
+static void ksmbd_tcp_tx_work(struct work_struct *work)
+{
+ struct tcp_transport *t = container_of(to_delayed_work(work),
+ struct tcp_transport, tx_work);
+ unsigned long delay = 0;
+ unsigned int budget = 16;
+ bool failed = false;
+ int ret;
+
+ mutex_lock(&t->tx_lock);
+ while (!READ_ONCE(t->tx_stopped) && !list_empty(&t->tx_queue) && budget--) {
+ struct tcp_tx *msg = list_first_entry(&t->tx_queue, struct tcp_tx, list);
+ struct msghdr hdr = {
+ .msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT | msg->msg_flags,
+ };
+ struct kvec iov = {
+ .iov_base = msg->data + msg->offset,
+ .iov_len = msg->len - msg->offset,
+ };
+
+ /* MSG_DONTWAIT bypasses sk_sndtimeo, including for retained PDUs. */
+ if (time_after_eq(jiffies, msg->deadline)) {
+ WRITE_ONCE(t->tx_stopped, true);
+ failed = true;
+ break;
+ }
+ ret = kernel_sendmsg(t->sock, &hdr, &iov, 1, iov.iov_len);
+ if (ret > 0) {
+ msg->offset += ret;
+ t->tx_sent += ret;
+ if (msg->offset == msg->len) {
+ WRITE_ONCE(t->tx_queued, t->tx_queued - msg->len);
+ list_del(&msg->list);
+ kvfree(msg);
+ continue;
+ }
+ } else if (!ksmbd_tcp_send_retry(ret)) {
+ WRITE_ONCE(t->tx_stopped, true);
+ failed = true;
+ break;
+ }
+ /*
+ * Write-space notifications normally resume the head of the queue.
+ * Also retry after memory pressure, which may produce no notification.
+ */
+ delay = max_t(unsigned long, 1, HZ / 10);
+ break;
+ }
+ if (READ_ONCE(t->tx_stopped))
+ ksmbd_tcp_purge_tx(t);
+ else if (!list_empty(&t->tx_queue))
+ queue_delayed_work(system_unbound_wq, &t->tx_work, delay);
+ mutex_unlock(&t->tx_lock);
+ wake_up_all(&t->transport.conn->req_running_q);
+
+ if (failed)
+ ksmbd_conn_abort(t->transport.conn);
+}
+
+static void ksmbd_tcp_write_space(struct sock *sk)
+{
+ struct tcp_transport *t;
+
+ read_lock_bh(&sk->sk_callback_lock);
+ t = sk->sk_user_data;
+ if (t) {
+ t->write_space(sk);
+ if (!READ_ONCE(t->tx_stopped) && sk_stream_is_writeable(sk))
+ mod_delayed_work(system_unbound_wq, &t->tx_work, 0);
+ }
+ read_unlock_bh(&sk->sk_callback_lock);
+}
+
+static void ksmbd_tcp_stop_tx(struct tcp_transport *t)
+{
+ struct sock *sk = t->sock->sk;
+
+ WRITE_ONCE(t->tx_stopped, true);
+ write_lock_bh(&sk->sk_callback_lock);
+ if (sk->sk_user_data == t) {
+ sk->sk_write_space = t->write_space;
+ sk->sk_user_data = NULL;
+ }
+ write_unlock_bh(&sk->sk_callback_lock);
+ cancel_delayed_work_sync(&t->tx_work);
+ mutex_lock(&t->tx_lock);
+ ksmbd_tcp_purge_tx(t);
+ mutex_unlock(&t->tx_lock);
+ wake_up_all(&t->transport.conn->req_running_q);
+}
+
static int ksmbd_tcp_writev(struct ksmbd_transport *t,
const struct ksmbd_transport_write *tx)
{
- struct msghdr smb_msg = {
- .msg_flags = MSG_NOSIGNAL | tx->msg_flags,
+ struct tcp_transport *tt = TCP_TRANS(t);
+ struct sock *sk = tt->sock->sk;
+ struct msghdr hdr = {
+ .msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT | tx->msg_flags,
};
+ struct tcp_tx *msg;
+ unsigned long deadline = jiffies + KSMBD_TCP_SEND_TIMEOUT;
+ long timeout;
+ int sent = 0;
+
+ for (;;) {
+ if (tx->msg_flags & MSG_DONTWAIT) {
+ if (!mutex_trylock(&tt->tx_lock))
+ return -EAGAIN;
+ } else {
+ mutex_lock(&tt->tx_lock);
+ }
+ if (READ_ONCE(tt->tx_stopped) || ksmbd_conn_exiting(t->conn) ||
+ ksmbd_conn_releasing(t->conn)) {
+ mutex_unlock(&tt->tx_lock);
+ return -EPIPE;
+ }
+ if (ksmbd_tcp_tx_has_space(tt, tx->size))
+ break;
+ mutex_unlock(&tt->tx_lock);
+ if (tx->msg_flags & MSG_DONTWAIT)
+ return -EAGAIN;
+ timeout = deadline - jiffies;
+ if (time_after_eq(jiffies, deadline) ||
+ !wait_event_timeout(t->conn->req_running_q,
+ READ_ONCE(tt->tx_stopped) ||
+ ksmbd_conn_exiting(t->conn) ||
+ ksmbd_conn_releasing(t->conn) ||
+ ksmbd_tcp_tx_has_space(tt, tx->size),
+ timeout)) {
+ /* A queued PDU may already have a prefix in the socket. */
+ ksmbd_conn_abort(t->conn);
+ return -EAGAIN;
+ }
+ }
- return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov,
- tx->iov_cnt, tx->size);
+ /* Avoid an extra copy when the socket accepts the whole PDU at once. */
+ if (list_empty(&tt->tx_queue)) {
+ sent = kernel_sendmsg(tt->sock, &hdr, tx->iov, tx->iov_cnt, tx->size);
+ if (sent > 0) {
+ tt->tx_sent += sent;
+ if (sent == tx->size)
+ goto accepted;
+ } else if (ksmbd_tcp_send_retry(sent)) {
+ sent = 0;
+ } else {
+ WRITE_ONCE(tt->tx_stopped, true);
+ mutex_unlock(&tt->tx_lock);
+ ksmbd_conn_abort(t->conn);
+ return sent ?: -EPIPE;
+ }
+ }
+
+ /* The request buffers can be freed as soon as writev returns. */
+ msg = ksmbd_tcp_alloc_tx(tx, sent);
+ if (!msg) {
+ if (sent)
+ WRITE_ONCE(tt->tx_stopped, true);
+ mutex_unlock(&tt->tx_lock);
+ if (sent)
+ ksmbd_conn_abort(t->conn);
+ return -ENOMEM;
+ }
+ msg->deadline = deadline;
+ list_add_tail(&msg->list, &tt->tx_queue);
+ WRITE_ONCE(tt->tx_queued, tt->tx_queued + msg->len);
+ queue_delayed_work(system_unbound_wq, &tt->tx_work, 0);
+
+accepted:
+ tt->tx_end += tx->size;
+ if (tx->tracking) {
+ /* Include retained data, without retaining the caller's tracking object. */
+ tx->tracking->end = tt->tx_end;
+ lock_sock(sk);
+ tx->tracking->retry_after =
+ clamp_t(unsigned long, 6UL * inet_csk(sk)->icsk_rto,
+ msecs_to_jiffies(1200), 6 * HZ);
+ release_sock(sk);
+ tx->tracking->valid = true;
+ }
+ mutex_unlock(&tt->tx_lock);
+ return tx->size;
+}
+
+static bool ksmbd_tcp_is_acked(struct ksmbd_transport *t,
+ const struct ksmbd_tx_tracking *tracking)
+{
+ struct tcp_transport *tt = TCP_TRANS(t);
+ struct sock *sk = tt->sock->sk;
+ bool acked;
+
+ mutex_lock(&tt->tx_lock);
+ lock_sock(sk);
+ acked = tt->tx_sent >= tracking->end &&
+ tcp_sk(sk)->bytes_acked >= tracking->end;
+ release_sock(sk);
+ mutex_unlock(&tt->tx_lock);
+ return acked;
}
static void ksmbd_tcp_disconnect(struct ksmbd_transport *t)
@@ -451,7 +729,9 @@ static void ksmbd_tcp_disconnect(struct ksmbd_transport *t)
static void ksmbd_tcp_shutdown(struct ksmbd_transport *t)
{
+ WRITE_ONCE(TCP_TRANS(t)->tx_stopped, true);
kernel_sock_shutdown(TCP_TRANS(t)->sock, SHUT_RDWR);
+ wake_up_all(&t->conn->req_running_q);
}
static void tcp_destroy_socket(struct socket *ksmbd_socket)
@@ -724,6 +1004,7 @@ int ksmbd_tcp_set_interfaces(char *ifc_list, int ifc_list_sz)
static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops = {
.read = ksmbd_tcp_read,
.writev = ksmbd_tcp_writev,
+ .is_acked = ksmbd_tcp_is_acked,
.disconnect = ksmbd_tcp_disconnect,
.shutdown = ksmbd_tcp_shutdown,
.free_transport = ksmbd_tcp_free_transport,
--
2.53.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 7.3.y 3/4] ksmbd: retry break notifications on another connection
2026-11-03 17:03 ` [PATCH 7.3.y 1/4] ksmbd: use clear_and_wake_up_bit() directly Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 2/4] ksmbd: track delivery of queued TCP writes Sasha Levin
@ 2026-11-03 17:03 ` Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 4/4] ksmbd: fix app-instance non-durable supersede session UAF Sasha Levin
2 siblings, 0 replies; 5+ messages in thread
From: Sasha Levin @ 2026-11-03 17:03 UTC (permalink / raw)
To: stable; +Cc: Namjae Jeon, Pan Guihua, Sasha Levin
From: Namjae Jeon <linkinjeon@kernel.org>
[ Upstream commit 14ad1f21fbf05179116dd6ef4ab3b6556478fa50 ]
An oplock or lease break belongs to an open that can outlive its original
channel. A draining channel or stalled TCP writer can otherwise leave the
break pending until timeout or invalidate a durable open.
Snapshot the owner's live channels and try eligible connections with the
same notification. Track TCP delivery separately from the SMB ACK, retry
when a channel stops progressing, and share one break deadline. Run
ordinary break waits on a dedicated workqueue so blocked request
responses do not stall breaks.
Publish NONE before sending a level II break, which requires no ACK, so
an unsolicited ACK observes the completed level while TCP delivery is
pending.
Keep the first rename lease notification in the request context and
enqueue the interim writer only after the notification is accepted. Join
that work before returning so a blocked interim response does not stop
notification retries or release the request early.
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
Stable-dep-of: f728cfee6929 ("ksmbd: fix app-instance non-durable supersede session UAF")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/smb/server/ksmbd_work.c | 19 +
fs/smb/server/ksmbd_work.h | 2 +
fs/smb/server/mgmt/user_session.c | 31 ++
fs/smb/server/mgmt/user_session.h | 3 +
fs/smb/server/oplock.c | 855 +++++++++++++++++-------------
fs/smb/server/oplock.h | 14 +-
fs/smb/server/vfs_cache.c | 2 +
7 files changed, 539 insertions(+), 387 deletions(-)
diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c
index d307aefe0aec1..b0285eb02f35c 100644
--- a/fs/smb/server/ksmbd_work.c
+++ b/fs/smb/server/ksmbd_work.c
@@ -16,6 +16,7 @@
static struct kmem_cache *work_cache;
static struct workqueue_struct *ksmbd_wq;
+static struct workqueue_struct *ksmbd_break_wq;
static int ksmbd_reserve_iov(struct ksmbd_work *work, int need_iov_cnt)
{
@@ -113,6 +114,12 @@ int ksmbd_workqueue_init(void)
ksmbd_wq = alloc_workqueue("ksmbd-io", WQ_PERCPU, 0);
if (!ksmbd_wq)
return -ENOMEM;
+ ksmbd_break_wq = alloc_workqueue("ksmbd-break", WQ_UNBOUND, 0);
+ if (!ksmbd_break_wq) {
+ destroy_workqueue(ksmbd_wq);
+ ksmbd_wq = NULL;
+ return -ENOMEM;
+ }
return 0;
}
@@ -120,6 +127,8 @@ void ksmbd_workqueue_destroy(void)
{
destroy_workqueue(ksmbd_wq);
ksmbd_wq = NULL;
+ destroy_workqueue(ksmbd_break_wq);
+ ksmbd_break_wq = NULL;
}
bool ksmbd_queue_work(struct ksmbd_work *work)
@@ -127,6 +136,16 @@ bool ksmbd_queue_work(struct ksmbd_work *work)
return queue_work(ksmbd_wq, &work->work);
}
+void ksmbd_queue_io_work(struct work_struct *work)
+{
+ queue_work(ksmbd_wq, work);
+}
+
+void ksmbd_queue_break_work(struct work_struct *work)
+{
+ queue_work(ksmbd_break_wq, work);
+}
+
static inline void __ksmbd_iov_pin(struct ksmbd_work *work, void *ib,
unsigned int ib_len)
{
diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h
index 0844aa929f55d..6877f68381186 100644
--- a/fs/smb/server/ksmbd_work.h
+++ b/fs/smb/server/ksmbd_work.h
@@ -156,6 +156,8 @@ int ksmbd_work_pool_init(void);
int ksmbd_workqueue_init(void);
void ksmbd_workqueue_destroy(void);
bool ksmbd_queue_work(struct ksmbd_work *work);
+void ksmbd_queue_io_work(struct work_struct *work);
+void ksmbd_queue_break_work(struct work_struct *work);
int ksmbd_iov_pin_rsp_read(struct ksmbd_work *work, void *ib, int len,
void *aux_buf, unsigned int aux_size);
int ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len);
diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c
index 63d455053cdd2..1b30e432ebfd8 100644
--- a/fs/smb/server/mgmt/user_session.c
+++ b/fs/smb/server/mgmt/user_session.c
@@ -767,6 +767,37 @@ struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id)
return sess;
}
+/* The caller holds a session reference and must put each returned connection. */
+unsigned int ksmbd_session_get_channels(struct ksmbd_session *sess,
+ struct ksmbd_conn **conns,
+ unsigned int max_conns)
+{
+ struct channel *chann;
+ unsigned long index;
+ unsigned int nr_conns = 0;
+
+ /*
+ * Channel connections are borrowed. Unregister can leave the channel
+ * list in a pinned session after removing it from the registry, so
+ * chann_lock alone does not keep those connections alive. Only collect
+ * registered sessions, excluding both unregister and connection cleanup
+ * until the connection references have been taken.
+ */
+ down_read(&sessions_table_lock);
+ if (hlist_unhashed(&sess->hlist))
+ goto out;
+ down_read(&sess->chann_lock);
+ xa_for_each(&sess->ksmbd_chann_list, index, chann) {
+ if (nr_conns == max_conns)
+ break;
+ conns[nr_conns++] = ksmbd_conn_get(chann->conn);
+ }
+ up_read(&sess->chann_lock);
+out:
+ up_read(&sessions_table_lock);
+ return nr_conns;
+}
+
struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn,
unsigned long long id)
{
diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h
index ef52d19e58977..8e4ddb04f0e6d 100644
--- a/fs/smb/server/mgmt/user_session.h
+++ b/fs/smb/server/mgmt/user_session.h
@@ -99,6 +99,9 @@ void ksmbd_session_release_slot(struct ksmbd_conn *conn);
void ksmbd_session_destroy(struct ksmbd_session *sess);
struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id);
+unsigned int ksmbd_session_get_channels(struct ksmbd_session *sess,
+ struct ksmbd_conn **conns,
+ unsigned int max_conns);
struct ksmbd_session *ksmbd_session_lookup(struct ksmbd_conn *conn,
unsigned long long id);
bool is_ksmbd_session_in_connection(struct ksmbd_conn *conn,
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 25ff8408f0ba2..313bf01ad3508 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -87,6 +87,7 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work,
return NULL;
opinfo->sess = sess;
+ opinfo->session_id = sess->id;
opinfo->conn = ksmbd_conn_get(work->conn);
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
opinfo->op_state = OPLOCK_STATE_NONE;
@@ -302,15 +303,9 @@ static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci,
opinfo = list_first_entry_or_null(&ci->m_op_list, struct oplock_info,
op_entry);
if (opinfo) {
- if (opinfo->conn == NULL ||
- !atomic_inc_not_zero(&opinfo->refcount))
+ /* A live open may still be bound to a disconnected channel. */
+ if (!atomic_inc_not_zero(&opinfo->refcount))
opinfo = NULL;
- else {
- if (ksmbd_conn_releasing(opinfo->conn)) {
- atomic_dec(&opinfo->refcount);
- opinfo = NULL;
- }
- }
if (opinfo && snapshot && opinfo->o_fp &&
opinfo->o_fp != skip_fp &&
@@ -367,7 +362,7 @@ static void opinfo_del(struct oplock_info *opinfo)
lease_del_open(opinfo);
down_write(&ci->m_lock);
- list_del(&opinfo->op_entry);
+ list_del_init(&opinfo->op_entry);
up_write(&ci->m_lock);
}
@@ -639,19 +634,27 @@ static void grant_none_oplock(struct oplock_info *opinfo_new,
static inline int compare_guid_key(struct oplock_info *opinfo,
const char *guid1, const char *key1)
{
- const char *guid2, *key2;
+ struct lease *lease = opinfo->o_lease;
+ struct lease_table *lb;
struct ksmbd_conn *conn;
+ int ret = 0;
- conn = READ_ONCE(opinfo->conn);
- if (!conn)
+ if (memcmp(key1, lease->lease_key, SMB2_LEASE_KEY_SIZE))
return 0;
- guid2 = conn->ClientGUID;
- key2 = opinfo->o_lease->lease_key;
- if (!memcmp(guid1, guid2, SMB2_CLIENT_GUID_SIZE) &&
- !memcmp(key1, key2, SMB2_LEASE_KEY_SIZE))
- return 1;
- return 0;
+ /* The caller holds ci->m_lock, which protects opinfo->conn. */
+ conn = READ_ONCE(opinfo->conn);
+ if (conn)
+ return !memcmp(guid1, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE);
+
+ /* A detached durable lease retains its client identity in the table. */
+ read_lock(&lease_list_lock);
+ lb = lease->l_lb;
+ if (lb)
+ ret = !memcmp(guid1, lb->client_guid, SMB2_CLIENT_GUID_SIZE);
+ read_unlock(&lease_list_lock);
+
+ return ret;
}
/**
@@ -681,7 +684,7 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci,
*/
down_read(&ci->m_lock);
list_for_each_entry(opinfo, &ci->m_op_list, op_entry) {
- if (!opinfo->is_lease || !opinfo->conn)
+ if (!opinfo->is_lease)
continue;
lease = opinfo->o_lease;
@@ -734,35 +737,6 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci,
return m_opinfo;
}
-static bool wait_for_break_ack(struct oplock_info *opinfo)
-{
- int rc = 0;
-
- rc = wait_event_interruptible_timeout(opinfo->oplock_q,
- opinfo->op_state == OPLOCK_STATE_NONE ||
- opinfo->op_state == OPLOCK_CLOSING,
- OPLOCK_WAIT_TIME);
-
- /* is this a timeout ? */
- if (!rc) {
- spin_lock(&opinfo->state_lock);
- if (opinfo->op_state == OPLOCK_CLOSING) {
- spin_unlock(&opinfo->state_lock);
- return false;
- }
- if (opinfo->is_lease) {
- opinfo->o_lease->state = SMB2_LEASE_NONE_LE;
- lease_update_oplock_levels(opinfo->o_lease);
- }
- opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
- opinfo->op_state = OPLOCK_STATE_NONE;
- spin_unlock(&opinfo->state_lock);
- return true;
- }
-
- return false;
-}
-
static bool oplock_break_set_ack_wait(struct oplock_info *opinfo)
{
bool ret = false;
@@ -842,330 +816,492 @@ static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level,
return opinfo->level > req_op_level;
}
-/**
- * __smb2_oplock_break_noti() - send smb2 oplock break cmd from conn
- * to client
- * @wk: smb work object
- *
- * There are two ways this function can be called. 1- while file open we break
- * from exclusive/batch lock to levelII oplock and 2- while file write/truncate
- * we break from levelII oplock no oplock.
- * work->request_buf contains oplock_info.
- */
-static void __smb2_oplock_break_noti(struct work_struct *wk)
+struct smb2_break_channel {
+ struct list_head list;
+ struct ksmbd_conn *conn;
+ struct ksmbd_tx_tracking tracking;
+ bool queued;
+ bool delivered;
+ bool failed;
+};
+
+static bool smb2_break_complete(struct oplock_info *opinfo, bool expect_ack)
{
- struct smb2_oplock_break *rsp = NULL;
- struct ksmbd_work *work = container_of(wk, struct ksmbd_work, work);
- struct ksmbd_conn *conn = work->conn;
- struct oplock_break_info *br_info = work->request_buf;
- struct smb2_hdr *rsp_hdr;
- struct ksmbd_file *fp;
+ bool complete;
- fp = ksmbd_lookup_global_fd(br_info->fid);
- if (!fp)
- goto out;
+ spin_lock(&opinfo->state_lock);
+ complete = opinfo->op_state == OPLOCK_CLOSING ||
+ (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE);
+ spin_unlock(&opinfo->state_lock);
+ return complete;
+}
- if (allocate_interim_rsp_buf(work)) {
- pr_err("smb2_allocate_rsp_buf failed! ");
- ksmbd_fd_put(work, fp);
- goto out;
+static int smb2_break_failed(struct oplock_info *opinfo, bool expect_ack)
+{
+ spin_lock(&opinfo->state_lock);
+ /* An ACK or close which won the race has already completed the break. */
+ if (opinfo->op_state == OPLOCK_CLOSING ||
+ (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) {
+ spin_unlock(&opinfo->state_lock);
+ return 0;
}
+ if (opinfo->is_lease) {
+ opinfo->o_lease->state = SMB2_LEASE_NONE_LE;
+ opinfo->o_lease->new_state = SMB2_LEASE_NONE_LE;
+ lease_update_oplock_levels(opinfo->o_lease);
+ atomic_set(&opinfo->breaking_cnt, 0);
+ }
+ opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ spin_unlock(&opinfo->state_lock);
+ wake_up_interruptible_all(&opinfo->oplock_q);
+ wake_up_interruptible_all(&opinfo->oplock_brk);
+ return ksmbd_invalidate_durable_fd(opinfo->fid);
+}
- rsp_hdr = smb_get_msg(work->response_buf);
- memset(rsp_hdr, 0, sizeof(struct smb2_hdr) + 2);
- rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER;
- rsp_hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE;
- rsp_hdr->CreditRequest = cpu_to_le16(0);
- rsp_hdr->Command = SMB2_OPLOCK_BREAK;
- rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR);
- rsp_hdr->NextCommand = 0;
- rsp_hdr->MessageId = cpu_to_le64(-1);
- rsp_hdr->Id.SyncId.ProcessId = 0;
- rsp_hdr->Id.SyncId.TreeId = 0;
- rsp_hdr->SessionId = 0;
- memset(rsp_hdr->Signature, 0, 16);
-
- rsp = smb_get_msg(work->response_buf);
-
- rsp->StructureSize = cpu_to_le16(24);
- if (!br_info->open_trunc &&
- (br_info->level == SMB2_OPLOCK_LEVEL_BATCH ||
- br_info->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE))
- rsp->OplockLevel = SMB2_OPLOCK_LEVEL_II;
- else
- rsp->OplockLevel = SMB2_OPLOCK_LEVEL_NONE;
- rsp->Reserved = 0;
- rsp->Reserved2 = 0;
- rsp->PersistentFid = fp->persistent_id;
- rsp->VolatileFid = fp->volatile_id;
-
- ksmbd_fd_put(work, fp);
- if (ksmbd_iov_pin_rsp(work, (void *)rsp,
- sizeof(struct smb2_oplock_break)))
- goto out;
-
- ksmbd_debug(OPLOCK,
- "sending oplock break v_id %llu p_id = %llu lock level = %d\n",
- rsp->VolatileFid, rsp->PersistentFid, rsp->OplockLevel);
-
- ksmbd_conn_write(work);
+static int smb2_break_add_channel(struct list_head *head, struct ksmbd_conn *conn)
+{
+ struct smb2_break_channel *channel;
-out:
- ksmbd_free_work_struct(work);
- ksmbd_conn_r_count_dec(conn);
- ksmbd_conn_put(conn);
+ channel = kzalloc_obj(*channel, KSMBD_DEFAULT_GFP);
+ if (!channel)
+ return -ENOMEM;
+ channel->conn = ksmbd_conn_get(conn);
+ list_add_tail(&channel->list, head);
+ return 0;
}
/*
- * Select and pin the connection used for an oplock break before doing any
- * allocations which may sleep. The caller of oplock_break() holds a live
- * reference on ci (a file being opened, a file being operated on, or an
- * explicit ksmbd_inode_lookup_lock() reference in the parent lease break
- * paths), so the inode cannot be freed during the call and its lock is
- * reachable without dereferencing opinfo->o_fp, which is not pinned by
- * the oplock reference and may be freed by a concurrent close.
- *
- * opinfo->conn is cleared under ci->m_lock by session_fd_check() when the
- * durable handle owning the oplock is disconnected, reassigned by
- * ksmbd_reopen_durable_fd() under the same lock, and the last
- * ksmbd_conn_put() of the old connection frees it. Holding the read lock
- * excludes both writers, so the connection cannot be freed while it is
- * selected.
+ * Snapshot connections before sending. References keep their transports alive,
+ * while r_count is taken separately for each write, under request_lock.
*/
-static struct ksmbd_conn *smb2_oplock_break_conn_get(struct oplock_info *opinfo,
- struct ksmbd_inode *ci)
-{
+static int smb2_break_collect_channels(struct oplock_info *opinfo,
+ struct ksmbd_inode *ci,
+ struct smb2_break_channel *owner,
+ struct list_head *head,
+ struct smb2_oplock_break *rsp)
+{
+ struct ksmbd_session *sess = NULL;
+ struct ksmbd_conn *session_conns[KSMBD_MAX_CHANNELS];
+ struct ksmbd_file_table *ft;
+ struct ksmbd_file *fp;
struct ksmbd_conn *conn;
+ struct lease_table *lb;
+ char guid[SMB2_CLIENT_GUID_SIZE];
+ u64 session_id = 0;
+ unsigned int nr_conns, i;
+ bool have_guid = false;
+ int bkt, ret = 0;
+ /* m_lock protects membership and binding, but not the session's lifetime. */
down_read(&ci->m_lock);
- conn = READ_ONCE(opinfo->conn);
- if (conn && !ksmbd_conn_releasing(conn))
- conn = ksmbd_conn_get(conn);
- else
- conn = NULL;
+ if (!list_empty(&opinfo->op_entry)) {
+ if (!opinfo->is_lease) {
+ rsp->PersistentFid = READ_ONCE(opinfo->o_fp->persistent_id);
+ rsp->VolatileFid = READ_ONCE(opinfo->o_fp->volatile_id);
+ if (!has_file_id(rsp->PersistentFid) ||
+ !has_file_id(rsp->VolatileFid)) {
+ /* A detached oplock must take the failed-break path. */
+ ret = opinfo->conn ? -ENOENT : -ENOTCONN;
+ goto unlock;
+ }
+ session_id = opinfo->session_id;
+ }
+ conn = opinfo->conn;
+ if (conn) {
+ owner->conn = ksmbd_conn_get(conn);
+ memcpy(guid, conn->ClientGUID, sizeof(guid));
+ have_guid = true;
+ }
+ } else {
+ ret = -ENOENT;
+ }
+unlock:
up_read(&ci->m_lock);
+ if (ret)
+ return ret;
+
+ if (opinfo->is_lease && !have_guid) {
+ read_lock(&lease_list_lock);
+ lb = opinfo->o_lease->l_lb;
+ if (lb) {
+ memcpy(guid, lb->client_guid, sizeof(guid));
+ have_guid = true;
+ if (lb->conn)
+ owner->conn = ksmbd_conn_get(lb->conn);
+ }
+ read_unlock(&lease_list_lock);
+ }
+ if (owner->conn)
+ list_add_tail(&owner->list, head);
+
+ if (!opinfo->is_lease) {
+ /* Session teardown takes m_lock while holding the session table lock. */
+ sess = ksmbd_session_lookup_slowpath(session_id);
+ if (!sess)
+ return -ENOENT;
+
+ /* Revalidate the binding and IDs after taking a safe session reference. */
+ down_read(&ci->m_lock);
+ if (list_empty(&opinfo->op_entry) || opinfo->sess != sess) {
+ ret = -ENOENT;
+ } else {
+ ft = &sess->file_table;
+ read_lock(&ft->lock);
+ fp = opinfo->o_fp;
+ if (fp->f_state == FP_CLOSED ||
+ !has_file_id(fp->volatile_id) ||
+ idr_find(ft->idr, fp->volatile_id) != fp) {
+ ret = -ENOENT;
+ } else {
+ rsp->PersistentFid = READ_ONCE(fp->persistent_id);
+ rsp->VolatileFid = fp->volatile_id;
+ }
+ read_unlock(&ft->lock);
+ }
+ up_read(&ci->m_lock);
+ if (ret) {
+ ksmbd_user_session_put(sess);
+ return ret;
+ }
+ }
- return conn;
+ if (opinfo->is_lease && have_guid) {
+ down_read(&conn_list_lock);
+ hash_for_each(conn_list, bkt, conn, hlist) {
+ if (conn == owner->conn || !ksmbd_conn_good(conn) ||
+ memcmp(conn->ClientGUID, guid, sizeof(guid)))
+ continue;
+ ret = smb2_break_add_channel(head, conn);
+ if (ret)
+ break;
+ }
+ up_read(&conn_list_lock);
+ } else if (!opinfo->is_lease && sess && sess->dialect >= SMB30_PROT_ID) {
+ nr_conns = ksmbd_session_get_channels(sess, session_conns,
+ ARRAY_SIZE(session_conns));
+ for (i = 0; i < nr_conns; i++) {
+ conn = session_conns[i];
+ if (!ret && conn && conn != owner->conn && ksmbd_conn_good(conn))
+ ret = smb2_break_add_channel(head, conn);
+ /* Drop all snapshot references, also on allocation failure. */
+ ksmbd_conn_put(conn);
+ }
+ }
+ ksmbd_user_session_put(sess);
+
+ /* Allocation pressure may limit retries, but must not discard the owner. */
+ return list_empty(head) ? (ret ?: -ENOTCONN) : 0;
}
-/**
- * smb2_oplock_break_noti() - send smb2 exclusive/batch to level2 oplock
- * break command from server to client
- * @opinfo: oplock info object
- * @ci: inode owning the break target's oplock list, pinned by
- * the caller
- *
- * Return: 0 on success, otherwise error
- */
-static int smb2_oplock_break_noti(struct oplock_info *opinfo,
- struct ksmbd_inode *ci)
+static bool smb2_break_delivered(struct list_head *head, bool require_live)
{
+ struct smb2_break_channel *channel;
struct ksmbd_conn *conn;
- struct oplock_break_info *br_info;
- int ret = 0;
- struct ksmbd_work *work;
-
- conn = smb2_oplock_break_conn_get(opinfo, ci);
- if (!conn)
- return ksmbd_invalidate_durable_fd(opinfo->fid);
- work = ksmbd_alloc_work_struct();
- if (!work) {
- ksmbd_conn_put(conn);
- return -ENOMEM;
+ list_for_each_entry(channel, head, list) {
+ conn = channel->conn;
+ if (!channel->queued || (require_live && !ksmbd_conn_good(conn)))
+ continue;
+ /* A synchronous send needs no separate delivery probe. */
+ if (channel->delivered || !channel->tracking.valid ||
+ conn->transport->ops->is_acked(conn->transport,
+ &channel->tracking)) {
+ channel->delivered = true;
+ return true;
+ }
}
+ return false;
+}
- br_info = kmalloc_obj(struct oplock_break_info, KSMBD_DEFAULT_GFP);
- if (!br_info) {
- ksmbd_free_work_struct(work);
- ksmbd_conn_put(conn);
- return -ENOMEM;
- }
+static bool smb2_break_has_unsent_channels(struct list_head *head)
+{
+ struct smb2_break_channel *channel;
- br_info->level = opinfo->level;
- br_info->fid = opinfo->fid;
- br_info->open_trunc = opinfo->open_trunc;
+ list_for_each_entry(channel, head, list) {
+ if (!channel->queued && !channel->failed)
+ return true;
+ }
+ return false;
+}
- work->request_buf = (char *)br_info;
- /* Transfer the reference acquired by smb2_oplock_break_conn_get(). */
- work->conn = conn;
- work->sess = opinfo->sess;
+static int smb2_break_wait(struct oplock_info *opinfo, struct list_head *head,
+ struct smb2_break_channel *sent_channel,
+ bool expect_ack, bool wait_ack, bool can_retry,
+ unsigned long retry_at, unsigned long deadline)
+{
+ unsigned long remaining;
+ long ret;
- ksmbd_conn_r_count_inc(conn);
- if (opinfo->op_state == OPLOCK_ACK_WAIT) {
- INIT_WORK(&work->work, __smb2_oplock_break_noti);
- ksmbd_queue_work(work);
+ for (;;) {
+ if (smb2_break_complete(opinfo, expect_ack))
+ return 0;
+ if (!wait_ack && smb2_break_delivered(head, false))
+ return 0;
+ if (time_after_eq(jiffies, deadline))
+ return -ETIMEDOUT;
+ /* TCP delivery followed by a slow SMB ACK is not a send failure. */
+ if (can_retry && !smb2_break_delivered(head, true) &&
+ ((sent_channel && !ksmbd_conn_good(sent_channel->conn)) ||
+ time_after_eq(jiffies, retry_at)))
+ return -EAGAIN;
- 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)
- opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ remaining = min_t(unsigned long, deadline - jiffies,
+ max_t(unsigned long, 1, HZ / 10));
+ ret = wait_event_interruptible_timeout(opinfo->oplock_q,
+ smb2_break_complete(opinfo, expect_ack),
+ remaining);
+ if (ret < 0)
+ return ret;
}
- return ret;
}
-/**
- * __smb2_lease_break_noti() - send lease break command from server
- * to client
- * @wk: smb work object
- */
-static void __smb2_lease_break_noti(struct work_struct *wk)
+static void smb2_break_init_header(struct smb2_hdr *hdr)
{
- struct smb2_lease_break *rsp = NULL;
- struct ksmbd_work *work = container_of(wk, struct ksmbd_work, work);
- struct ksmbd_conn *conn = work->conn;
- struct lease_break_info *br_info = work->request_buf;
- struct smb2_hdr *rsp_hdr;
+ memset(hdr, 0, sizeof(*hdr));
+ hdr->ProtocolId = SMB2_PROTO_NUMBER;
+ hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE;
+ hdr->Command = SMB2_OPLOCK_BREAK;
+ hdr->Flags = SMB2_FLAGS_SERVER_TO_REDIR;
+ hdr->MessageId = cpu_to_le64(-1);
+}
- if (allocate_interim_rsp_buf(work)) {
- ksmbd_debug(OPLOCK, "smb2_allocate_rsp_buf failed! ");
- goto out;
+/* Build one notification and reuse its payload and epoch on each connection. */
+static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
+ bool expect_ack, bool wait_ack, bool inc_epoch,
+ struct work_struct *interim_work)
+{
+ struct smb2_break_channel owner = {}, *channel, *tmp, *last = NULL;
+ struct ksmbd_work *work;
+ struct smb2_oplock_break *oplock_rsp;
+ struct smb2_lease_break *lease_rsp;
+ struct lease *lease = opinfo->o_lease;
+ unsigned long deadline, retry_at;
+ int ret, size;
+ LIST_HEAD(channels);
+
+ if (smb2_break_complete(opinfo, expect_ack)) {
+ ret = 0;
+ goto out_interim;
+ }
+ wait_ack = wait_ack && expect_ack;
+ work = ksmbd_alloc_work_struct();
+ if (!work) {
+ ret = smb2_break_failed(opinfo, expect_ack);
+ goto out_interim;
}
+ ret = allocate_interim_rsp_buf(work);
+ if (ret)
+ goto failed;
+ oplock_rsp = smb_get_msg(work->response_buf);
+ smb2_break_init_header(&oplock_rsp->hdr);
- rsp_hdr = smb_get_msg(work->response_buf);
- memset(rsp_hdr, 0, sizeof(struct smb2_hdr) + 2);
- rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER;
- rsp_hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE;
- rsp_hdr->CreditRequest = cpu_to_le16(0);
- rsp_hdr->Command = SMB2_OPLOCK_BREAK;
- rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR);
- rsp_hdr->NextCommand = 0;
- rsp_hdr->MessageId = cpu_to_le64(-1);
- rsp_hdr->Id.SyncId.ProcessId = 0;
- rsp_hdr->Id.SyncId.TreeId = 0;
- rsp_hdr->SessionId = 0;
- memset(rsp_hdr->Signature, 0, 16);
-
- rsp = smb_get_msg(work->response_buf);
- rsp->StructureSize = cpu_to_le16(44);
- rsp->Epoch = br_info->epoch;
- rsp->Flags = 0;
-
- if (br_info->curr_state & (SMB2_LEASE_WRITE_CACHING_LE |
- SMB2_LEASE_HANDLE_CACHING_LE))
- rsp->Flags = SMB2_NOTIFY_BREAK_LEASE_FLAG_ACK_REQUIRED;
-
- memcpy(rsp->LeaseKey, br_info->lease_key, SMB2_LEASE_KEY_SIZE);
- rsp->CurrentLeaseState = br_info->curr_state;
- rsp->NewLeaseState = br_info->new_state;
- rsp->BreakReason = 0;
- rsp->AccessMaskHint = 0;
- rsp->ShareMaskHint = 0;
-
- if (ksmbd_iov_pin_rsp(work, (void *)rsp,
- sizeof(struct smb2_lease_break)))
+ ret = smb2_break_collect_channels(opinfo, ci, &owner, &channels, oplock_rsp);
+ if (ret == -ENOENT)
goto out;
+ if (ret)
+ goto failed;
- ksmbd_conn_write(work);
-
+ spin_lock(&opinfo->state_lock);
+ if (opinfo->op_state == OPLOCK_CLOSING ||
+ (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) {
+ spin_unlock(&opinfo->state_lock);
+ ret = 0;
+ goto out;
+ }
+ if (opinfo->is_lease) {
+ lease_rsp = smb_get_msg(work->response_buf);
+ lease_rsp->StructureSize = cpu_to_le16(44);
+ if (lease->version == 2) {
+ if (inc_epoch)
+ lease->epoch++;
+ lease_rsp->Epoch = cpu_to_le16(lease->epoch);
+ }
+ if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE |
+ SMB2_LEASE_HANDLE_CACHING_LE))
+ lease_rsp->Flags = SMB2_NOTIFY_BREAK_LEASE_FLAG_ACK_REQUIRED;
+ memcpy(lease_rsp->LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE);
+ lease_rsp->CurrentLeaseState = lease->state;
+ lease_rsp->NewLeaseState = lease->new_state;
+ size = sizeof(*lease_rsp);
+ } else {
+ oplock_rsp->StructureSize = cpu_to_le16(24);
+ oplock_rsp->OplockLevel = !opinfo->open_trunc &&
+ (opinfo->level == SMB2_OPLOCK_LEVEL_BATCH ||
+ opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) ?
+ SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE;
+ /*
+ * A level II break needs no ACK. Publish NONE before sending so
+ * an unsolicited ACK cannot observe II while delivery is pending.
+ */
+ if (!expect_ack)
+ opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ size = sizeof(*oplock_rsp);
+ }
+ spin_unlock(&opinfo->state_lock);
+ ret = ksmbd_iov_pin_rsp(work, oplock_rsp, size);
+ if (ret)
+ goto failed;
+
+ deadline = jiffies + OPLOCK_WAIT_TIME;
+retry:
+ list_for_each_entry(channel, &channels, list) {
+ if (channel->queued || channel->failed)
+ continue;
+ if (smb2_break_complete(opinfo, expect_ack)) {
+ ret = 0;
+ goto out;
+ }
+ if (time_after_eq(jiffies, deadline)) {
+ ret = -ETIMEDOUT;
+ break;
+ }
+ if (!ksmbd_conn_r_count_inc_if_good(channel->conn)) {
+ channel->failed = true;
+ ret = -ENOTCONN;
+ continue;
+ }
+ work->conn = channel->conn;
+ ret = ksmbd_conn_write_tracked(work, &channel->tracking);
+ ksmbd_conn_r_count_dec(channel->conn);
+ if (ret) {
+ /* Only a nonblocking send guarantees no bytes on -EAGAIN. */
+ if (ret != -EAGAIN || !channel->conn->transport->ops->is_acked)
+ channel->failed = true;
+ continue;
+ }
+ channel->queued = true;
+ if (interim_work) {
+ ksmbd_queue_io_work(interim_work);
+ interim_work = NULL;
+ }
+ last = channel;
+ retry_at = jiffies + channel->tracking.retry_after;
+ ret = smb2_break_wait(opinfo, &channels, channel, expect_ack,
+ wait_ack, smb2_break_has_unsent_channels(&channels),
+ retry_at, deadline);
+ if (ret != -EAGAIN)
+ goto done;
+ }
+ if (smb2_break_has_unsent_channels(&channels)) {
+ /* Allow busy writers to make progress before another nonblocking pass. */
+ retry_at = jiffies + max_t(unsigned long, 1, HZ / 10);
+ ret = smb2_break_wait(opinfo, &channels, NULL, expect_ack,
+ wait_ack, true, retry_at, deadline);
+ if (ret == -EAGAIN)
+ goto retry;
+ } else if (last) {
+ /* Give already queued notifications the rest of the shared wait. */
+ ret = smb2_break_wait(opinfo, &channels, last, expect_ack,
+ wait_ack, false, deadline, deadline);
+ }
+
+done:
+ if (ret)
+ goto failed;
+ if (!expect_ack && opinfo->is_lease) {
+ spin_lock(&opinfo->state_lock);
+ if (opinfo->op_state != OPLOCK_CLOSING) {
+ lease->state = lease->new_state;
+ lease_update_oplock_levels(lease);
+ }
+ spin_unlock(&opinfo->state_lock);
+ }
+ goto out;
+failed:
+ ret = smb2_break_failed(opinfo, expect_ack);
out:
ksmbd_free_work_struct(work);
- ksmbd_conn_r_count_dec(conn);
- ksmbd_conn_put(conn);
+ list_for_each_entry_safe(channel, tmp, &channels, list) {
+ list_del(&channel->list);
+ ksmbd_conn_put(channel->conn);
+ if (channel != &owner)
+ kfree(channel);
+ }
+out_interim:
+ if (interim_work)
+ ksmbd_queue_io_work(interim_work);
+ return ret;
}
-/*
- * Select and pin the connection used for a lease break before doing any
- * allocations which may sleep. opinfo->conn is cleared under ci->m_lock,
- * while lease->l_lb and the lease table lifetime are protected by
- * lease_list_lock.
- */
-static struct ksmbd_conn *smb2_lease_break_conn_get(struct oplock_info *opinfo)
+struct smb2_interim_work {
+ struct work_struct work;
+ struct ksmbd_work *in_work;
+};
+
+static void smb2_break_interim_work(struct work_struct *work)
{
- struct lease *lease = opinfo->o_lease;
- struct lease_table *lb;
- struct ksmbd_conn *conn;
+ struct smb2_interim_work *iw = container_of(work, struct smb2_interim_work, work);
- /* Keep the connection which owns the open, when it is still active. */
- down_read(&lease->ci->m_lock);
- conn = READ_ONCE(opinfo->conn);
- if (conn && !ksmbd_conn_releasing(conn))
- conn = ksmbd_conn_get(conn);
- else
- conn = NULL;
- up_read(&lease->ci->m_lock);
+ if (!setup_async_work(iw->in_work, NULL, NULL)) {
+ smb2_send_interim_resp(iw->in_work, STATUS_PENDING);
+ release_async_work(iw->in_work);
+ }
+}
- if (conn || lease->version != 2)
- return conn;
+struct smb2_break_work {
+ struct work_struct work;
+ struct oplock_info *opinfo;
+ struct ksmbd_inode *ci;
+ bool expect_ack;
+ bool wait_ack;
+ bool inc_epoch;
+ int ret;
+};
- /* Otherwise route v2 lease breaks through the shared lease channel. */
- read_lock(&lease_list_lock);
- lb = lease->l_lb;
- if (lb && lb->conn && !ksmbd_conn_releasing(lb->conn))
- conn = ksmbd_conn_get(lb->conn);
- read_unlock(&lease_list_lock);
+static void smb2_break_notify_work(struct work_struct *work)
+{
+ struct smb2_break_work *bw = container_of(work, struct smb2_break_work, work);
- return conn;
+ bw->ret = smb2_break_notify(bw->opinfo, bw->ci, bw->expect_ack,
+ bw->wait_ack, bw->inc_epoch, NULL);
}
-/**
- * smb2_lease_break_noti() - break lease when a new client request
- * write lease
- * @opinfo: contains lease state information
- * @sync: send the lease break notification synchronously
- * @inc_epoch: increment the lease epoch before sending the break
- *
- * Return: 0 on success, otherwise error
- */
-static int smb2_lease_break_noti(struct oplock_info *opinfo, bool sync,
- bool inc_epoch)
+static int smb2_break_notify_with_interim(struct oplock_info *opinfo,
+ struct ksmbd_inode *ci,
+ bool expect_ack, bool wait_ack,
+ bool inc_epoch, struct ksmbd_work *in_work,
+ bool notify_first)
{
- struct ksmbd_conn *conn;
- struct ksmbd_work *work;
- struct lease_break_info *br_info;
- struct lease *lease = opinfo->o_lease;
+ struct smb2_break_work bw = {
+ .opinfo = opinfo,
+ .ci = ci,
+ .expect_ack = expect_ack,
+ .wait_ack = wait_ack,
+ .inc_epoch = inc_epoch,
+ };
- conn = smb2_lease_break_conn_get(opinfo);
- if (!conn)
- return ksmbd_invalidate_durable_fd(opinfo->fid);
+ if (!in_work)
+ return smb2_break_notify(opinfo, ci, expect_ack, wait_ack, inc_epoch, NULL);
- work = ksmbd_alloc_work_struct();
- if (!work) {
- ksmbd_conn_put(conn);
- return -ENOMEM;
- }
+ if (notify_first) {
+ struct smb2_interim_work iw = { .in_work = in_work };
+ int ret;
- br_info = kmalloc_obj(struct lease_break_info, KSMBD_DEFAULT_GFP);
- if (!br_info) {
- ksmbd_free_work_struct(work);
- ksmbd_conn_put(conn);
- return -ENOMEM;
- }
-
- br_info->curr_state = lease->state;
- br_info->new_state = lease->new_state;
- if (lease->version == 2) {
- if (inc_epoch)
- lease->epoch++;
- br_info->epoch = cpu_to_le16(lease->epoch);
- } else {
- br_info->epoch = 0;
+ /*
+ * Rename sends the first notification in the request context.
+ * Queue the interim writer after that send so a blocked response
+ * cannot stop notification retries or the break ACK wait.
+ */
+ INIT_WORK_ONSTACK(&iw.work, smb2_break_interim_work);
+ ret = smb2_break_notify(opinfo, ci, expect_ack, wait_ack,
+ inc_epoch, &iw.work);
+ flush_work(&iw.work);
+ destroy_work_on_stack(&iw.work);
+ return ret;
}
- memcpy(br_info->lease_key, lease->lease_key, SMB2_LEASE_KEY_SIZE);
- work->request_buf = (char *)br_info;
- /* Transfer the reference acquired by smb2_lease_break_conn_get(). */
- work->conn = conn;
- work->sess = opinfo->sess;
-
- ksmbd_conn_r_count_inc(conn);
- if (opinfo->op_state == OPLOCK_ACK_WAIT) {
- if (sync) {
- __smb2_lease_break_noti(&work->work);
- } else {
- INIT_WORK(&work->work, __smb2_lease_break_noti);
- ksmbd_queue_work(work);
- }
- } else {
- __smb2_lease_break_noti(&work->work);
- if (opinfo->o_lease->new_state == SMB2_LEASE_NONE_LE) {
- opinfo->o_lease->state = SMB2_LEASE_NONE_LE;
- lease_update_oplock_levels(opinfo->o_lease);
- }
+ /* A blocked interim writer must not stop notification sends or retries. */
+ INIT_WORK_ONSTACK(&bw.work, smb2_break_notify_work);
+ ksmbd_queue_break_work(&bw.work);
+ if (!setup_async_work(in_work, NULL, NULL)) {
+ smb2_send_interim_resp(in_work, STATUS_PENDING);
+ release_async_work(in_work);
}
- return 0;
+ /* The caller keeps opinfo and ci alive until the notifier has finished. */
+ flush_work(&bw.work);
+ destroy_work_on_stack(&bw.work);
+ return bw.ret;
}
static void wait_lease_breaking(struct oplock_info *opinfo)
@@ -1187,10 +1323,11 @@ static void wait_lease_breaking(struct oplock_info *opinfo)
static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
int req_op_level, struct ksmbd_work *in_work,
- bool share_break, bool sync_lease_break)
+ bool share_break, bool notify_first)
{
int err = 0;
bool sent_interim = false;
+ bool expect_ack = false;
/* Need to break exclusive/batch oplock, write lease or overwrite_if */
ksmbd_debug(OPLOCK,
@@ -1248,8 +1385,9 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
}
}
- if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE |
- SMB2_LEASE_HANDLE_CACHING_LE)) {
+ expect_ack = lease->state & (SMB2_LEASE_WRITE_CACHING_LE |
+ SMB2_LEASE_HANDLE_CACHING_LE);
+ if (expect_ack) {
if (!oplock_break_set_ack_wait(brk_opinfo)) {
atomic_dec_if_positive(&brk_opinfo->breaking_cnt);
clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
@@ -1265,16 +1403,12 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
inc_epoch = false;
lease->reuse_epoch = false;
}
- err = smb2_lease_break_noti(brk_opinfo, sync_lease_break, inc_epoch);
+ err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack,
+ wait_ack, inc_epoch,
+ sent_interim ? NULL : in_work,
+ notify_first);
+ sent_interim = true;
inc_epoch = false;
- if (in_work && !sent_interim) {
- setup_async_work(in_work, NULL, NULL);
- smb2_send_interim_resp(in_work, STATUS_PENDING);
- release_async_work(in_work);
- sent_interim = true;
- }
- if (wait_ack && !err && wait_for_break_ack(brk_opinfo))
- err = ksmbd_invalidate_durable_fd(brk_opinfo->fid);
ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level);
if (brk_opinfo->op_state == OPLOCK_CLOSING)
@@ -1310,26 +1444,16 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH ||
brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) {
+ expect_ack = true;
if (!oplock_break_set_ack_wait(brk_opinfo)) {
clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return -ENOENT;
}
}
-
- /*
- * Keep a conflicting CREATE asynchronous while waiting for an
- * oplock-break acknowledgement. Besides avoiding a blocked client
- * request, this lets a replay arrive while the original CREATE is
- * still pending and be rejected with FILE_NOT_AVAILABLE.
- */
- if (in_work) {
- setup_async_work(in_work, NULL, NULL);
- smb2_send_interim_resp(in_work, STATUS_PENDING);
- release_async_work(in_work);
- }
}
- err = smb2_oplock_break_noti(brk_opinfo, ci);
+ err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack,
+ true, false, in_work, false);
ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level);
if (brk_opinfo->op_state == OPLOCK_CLOSING)
@@ -1491,7 +1615,7 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
down_read(&p_ci->m_lock);
list_for_each_entry(opinfo, &p_ci->m_op_list, op_entry) {
- if (opinfo->conn == NULL || !opinfo->is_lease)
+ if (!opinfo->is_lease)
continue;
if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE &&
@@ -1502,11 +1626,6 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
if (!atomic_inc_not_zero(&opinfo->refcount))
continue;
- if (ksmbd_conn_releasing(opinfo->conn)) {
- opinfo_put(opinfo);
- continue;
- }
-
if (oplock_break_add(&brk_list, opinfo))
opinfo_put(opinfo);
}
@@ -1539,18 +1658,13 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp)
down_read(&p_ci->m_lock);
list_for_each_entry(opinfo, &p_ci->m_op_list, op_entry) {
- if (opinfo->conn == NULL || !opinfo->is_lease)
+ if (!opinfo->is_lease)
continue;
if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE) {
if (!atomic_inc_not_zero(&opinfo->refcount))
continue;
- if (ksmbd_conn_releasing(opinfo->conn)) {
- opinfo_put(opinfo);
- continue;
- }
-
if (oplock_break_add(&brk_list, opinfo))
opinfo_put(opinfo);
}
@@ -1699,7 +1813,11 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid,
err = oplock_break(prev_opinfo, ci, break_level, work,
share_ret < 0 && prev_opinfo->is_lease, false);
- if (prev_durable_detached || (prev_durable_open && err == -ENOENT))
+ /* Keep a detached durable lease if its break retains handle caching. */
+ if ((prev_durable_detached &&
+ (!prev_op_has_lease ||
+ !(prev_opinfo->o_lease->state & SMB2_LEASE_HANDLE_CACHING_LE))) ||
+ (prev_durable_open && err == -ENOENT))
ksmbd_invalidate_durable_fd(prev_fid);
opinfo_put(prev_opinfo);
if (err == -EAGAIN) {
@@ -1803,8 +1921,7 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work,
}
brk_opinfo->open_trunc = is_trunc;
- oplock_break(brk_opinfo, fp->f_ci, SMB2_OPLOCK_LEVEL_II, work, false,
- false);
+ oplock_break(brk_opinfo, fp->f_ci, SMB2_OPLOCK_LEVEL_II, work, false, false);
sent_break = true;
opinfo_put(brk_opinfo);
@@ -1819,12 +1936,12 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work,
* @is_trunc: truncate on open
* @send_interim: send interim response to the client
* @send_oplock_break: send oplock break notification to the client
- * @sync_lease_break: send the lease break notification synchronously
+ * @notify_first: send the lease break notification before the interim response
*/
static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
struct ksmbd_file *fp, int is_trunc,
bool send_interim, bool send_oplock_break,
- bool sync_lease_break)
+ bool notify_first)
{
struct oplock_info *op, *brk_op;
struct oplock_break_entry *ent, *tmp;
@@ -1842,17 +1959,9 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
down_read(&ci->m_lock);
list_for_each_entry(brk_op, &ci->m_op_list, op_entry) {
- if (brk_op->conn == NULL)
- continue;
-
if (!atomic_inc_not_zero(&brk_op->refcount))
continue;
- if (ksmbd_conn_releasing(brk_op->conn)) {
- opinfo_put(brk_op);
- continue;
- }
-
if (!brk_op->is_lease &&
brk_op->level != SMB2_OPLOCK_LEVEL_II) {
ksmbd_debug(OPLOCK, "unexpected oplock(0x%x)\n",
@@ -1867,10 +1976,8 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
goto next;
if (op && op->is_lease && brk_op->is_lease &&
- !memcmp(conn->ClientGUID, brk_op->conn->ClientGUID,
- SMB2_CLIENT_GUID_SIZE) &&
- !memcmp(op->o_lease->lease_key, brk_op->o_lease->lease_key,
- SMB2_LEASE_KEY_SIZE))
+ compare_guid_key(brk_op, conn->ClientGUID,
+ op->o_lease->lease_key))
goto next;
brk_op->open_trunc = is_trunc;
@@ -1900,7 +2007,7 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
brk_op->is_lease && !is_trunc ?
SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE,
send_interim && !sent_interim ? work : NULL,
- false, sync_lease_break);
+ false, notify_first);
}
sent_interim = true;
list_del(&ent->list);
diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h
index b08d21758e07a..e114e2148c30e 100644
--- a/fs/smb/server/oplock.h
+++ b/fs/smb/server/oplock.h
@@ -62,6 +62,7 @@ struct lease {
struct oplock_info {
struct ksmbd_conn *conn;
struct ksmbd_session *sess;
+ u64 session_id;
struct ksmbd_work *work;
struct ksmbd_file *o_fp;
int level;
@@ -82,19 +83,6 @@ struct oplock_info {
struct rcu_head rcu;
};
-struct lease_break_info {
- __le32 curr_state;
- __le32 new_state;
- __le16 epoch;
- char lease_key[SMB2_LEASE_KEY_SIZE];
-};
-
-struct oplock_break_info {
- int level;
- int open_trunc;
- int fid;
-};
-
int smb_grant_oplock(struct ksmbd_work *work, int req_op_level,
u64 pid, struct ksmbd_file *fp, __u16 tid,
struct lease_ctx_info *lctx, int share_ret, bool replay);
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index d40196948efe7..a2051dfb47d91 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -1815,6 +1815,7 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
ksmbd_conn_put(op->conn);
op->conn = NULL;
op->sess = NULL;
+ op->session_id = 0;
}
up_write(&ci->m_lock);
@@ -1995,6 +1996,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
continue;
op->conn = ksmbd_conn_get(fp->conn);
op->sess = work->sess;
+ op->session_id = work->sess->id;
}
up_write(&ci->m_lock);
--
2.53.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 7.3.y 4/4] ksmbd: fix app-instance non-durable supersede session UAF
2026-11-03 17:03 ` [PATCH 7.3.y 1/4] ksmbd: use clear_and_wake_up_bit() directly Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 2/4] ksmbd: track delivery of queued TCP writes Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 3/4] ksmbd: retry break notifications on another connection Sasha Levin
@ 2026-11-03 17:03 ` Sasha Levin
2 siblings, 0 replies; 5+ messages in thread
From: Sasha Levin @ 2026-11-03 17:03 UTC (permalink / raw)
To: stable; +Cc: Ian Bridges, Namjae Jeon, Sasha Levin
From: Ian Bridges <icb@fastmail.org>
[ Upstream commit f728cfee6929057087c303d07d7a6a7c61196b48 ]
ksmbd_close_fd_app_instance_id() supersedes a prior open that has the
same AppInstanceId. It reaches that open's session through opinfo->sess.
This pointer is not refcounted.
Session teardown detaches durable handles before it frees the session.
It does not detach a non-durable open. That open still points at the
session through opinfo->sess after the session is freed. Another session
can supersede the open at the same time. The supersede then takes a
write lock through the stale pointer, in the freed session's file table.
BUG: KASAN: slab-use-after-free in _raw_write_lock+0x71/0xd0
Call Trace:
_raw_write_lock+0x71/0xd0
ksmbd_close_fd_app_instance_id+0xd9/0x670 [ksmbd]
smb2_open+0x3e10/0xd910 [ksmbd]
Freed by task 212:
ksmbd_session_destroy+0x38d/0x480 [ksmbd]
Fix this by looking the session up by opinfo->session_id. The lookup
returns a reference, as smb2_break_collect_channels() does. Recheck that
opinfo->sess still points at it before using it.
Do the lookup without m_lock held. destroy_previous_session() takes
m_lock under the session table lock, and taking them in the other order
here would deadlock.
Fixes: 16c30649709d ("ksmbd: handle durable v2 app instance id")
Cc: stable@vger.kernel.org # e0386cd6498b: ksmbd: validate AppInstanceId takeover target
Signed-off-by: Ian Bridges <icb@fastmail.org>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/smb/server/vfs_cache.c | 32 ++++++++++++++++++++++++++++++--
1 file changed, 30 insertions(+), 2 deletions(-)
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index a2051dfb47d91..5f1442c103d81 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -1054,7 +1054,9 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
struct ksmbd_file *fp, bool *closed)
{
struct ksmbd_file_table *ft;
+ struct ksmbd_session *sess;
struct oplock_info *opinfo;
+ u64 session_id;
int n_to_drop = 0;
*closed = false;
@@ -1067,10 +1069,35 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
up_read(&fp->f_ci->m_lock);
goto out;
}
+ /*
+ * opinfo->sess is not refcounted, and m_lock does not keep it alive.
+ * The owner can be torn down while we hold only a reference to fp.
+ * Look the owner up by id to take a real reference instead.
+ */
+ session_id = opinfo->session_id;
+ up_read(&fp->f_ci->m_lock);
- ft = &opinfo->sess->file_table;
+ /* destroy_previous_session() takes m_lock under sessions_table_lock. */
+ sess = ksmbd_session_lookup_slowpath(session_id);
+ if (!sess)
+ goto out;
+
+ down_read(&fp->f_ci->m_lock);
+ if (opinfo->sess != sess) {
+ up_read(&fp->f_ci->m_lock);
+ ksmbd_user_session_put(sess);
+ goto out;
+ }
+ ft = &sess->file_table;
write_lock(&ft->lock);
- if (fp->f_state == FP_INITED && has_file_id(fp->volatile_id) &&
+ /*
+ * The table may already be detached by destroy_previous_session().
+ * A new session could also reuse the old id and address and pass the
+ * check above. Only remove fp if this table still publishes it.
+ */
+ if (ft->idr && fp->f_state == FP_INITED &&
+ has_file_id(fp->volatile_id) &&
+ idr_find(ft->idr, fp->volatile_id) == fp &&
ksmbd_app_instance_id_matches(work, fp)) {
idr_remove(ft->idr, fp->volatile_id);
fp->volatile_id = KSMBD_NO_FID;
@@ -1078,6 +1105,7 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
}
write_unlock(&ft->lock);
up_read(&fp->f_ci->m_lock);
+ ksmbd_user_session_put(sess);
opinfo_put(opinfo);
opinfo = NULL;
--
2.53.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-03 17:03 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-03 10:44 FAILED: patch "[PATCH] ksmbd: fix app-instance non-durable supersede session UAF" failed to apply to 7.3-stable tree gregkh
2026-11-03 17:03 ` [PATCH 7.3.y 1/4] ksmbd: use clear_and_wake_up_bit() directly Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 2/4] ksmbd: track delivery of queued TCP writes Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 3/4] ksmbd: retry break notifications on another connection Sasha Levin
2026-11-03 17:03 ` [PATCH 7.3.y 4/4] ksmbd: fix app-instance non-durable supersede session UAF Sasha Levin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox